Built-in Middleware Patterns in Hyperlane
Project Code:https://github.com/hyperlane-dev/hyperlane
Hyperlane is a lightweight, high-performance, cross-platform Rust HTTP server library built on top of Tokio. One of its most powerful features is the middleware system, which allows developers to intercept and manipulate requests and responses in a clean, composable way. In this article, we will explore four essential middleware patterns that you can use to build robust web services: Authentication Middleware, CORS Middleware, Static File Middleware, and Timeout Middleware.
Table of Contents
- Overview of the Middleware System
- Authentication Middleware
- CORS Middleware
- Static File Middleware
- Timeout Middleware
- Combining Multiple Middlewares
- Conclusion
Overview of the Middleware System
Hyperlane's middleware system is built around the ServerHook trait. A middleware struct implements ServerHook, which provides two key methods:
-
new(): Called when a new connection is established. Use it to initialize per-connection state. -
handle(): Called during request processing. Use it to inspect or modify the request/response and decide whether to continue or reject.
Each middleware returns a Status enum value:
-
Status::Continue— pass the request to the next middleware or handler. -
Status::Reject— reject the request immediately.
Middlewares are registered on the server using dedicated methods like request_middleware, response_middleware, or via attribute macros like #[request_middleware(N)] and #[response_middleware].
struct RequestMiddleware;
impl ServerHook for RequestMiddleware {
async fn new(stream: &mut Stream, _: &mut Context) -> Self {
Self
}
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
ctx.get_mut_response()
.set_version(HttpVersion::Http1_1)
.set_status_code(200);
Status::Continue
}
}
server.request_middleware::<RequestMiddleware>();
struct ResponseMiddleware;
impl ServerHook for ResponseMiddleware {
async fn new(_: &mut Stream, _: &mut Context) -> Self {
Self
}
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
let data: Vec<u8> = ctx.get_mut_response().build();
if stream.try_send(data).await.is_err() {
stream.set_closed(true);
return Status::Reject;
}
Status::Continue
}
}
server.response_middleware::<ResponseMiddleware>();
You can also use attribute macros for a declarative approach:
#[request_middleware(1)]
struct RequestMiddleware1;
#[request_middleware(2)]
struct RequestMiddleware2;
#[response_middleware]
struct ResponseMiddleware;
The numeric argument to #[request_middleware(N)] controls execution order — lower numbers run first.
Authentication Middleware
Authentication is a fundamental requirement for most web applications. Hyperlane makes it straightforward to build an authentication middleware that inspects incoming requests before they reach your business logic.
How It Works
The authentication middleware reads the Authorization header from the incoming request. If the header is missing or empty, it immediately returns a 401 Unauthorized response and rejects the connection. Otherwise, it passes the request through.
impl ServerHook for AuthMiddleware {
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
let auth_str: String = ctx
.get_request()
.try_get_header_back(AUTHORIZATION)
.unwrap_or_default();
if auth_str.is_empty() {
let data: Vec<u8> = ctx
.get_mut_response()
.set_status_code(401)
.set_body("Unauthorized")
.build();
if stream.try_send(data).await.is_err() {
stream.set_closed(true);
}
return Status::Reject;
}
Status::Continue
}
}
Key Details
-
try_get_header_back(AUTHORIZATION)— Attempts to retrieve theAuthorizationheader. ReturnsNoneif the header is absent. -
unwrap_or_default()— ConvertsNoneto an empty string, which triggers the rejection path. -
Status::Reject— Stops the middleware chain and closes the request. No further handlers are invoked.
Extending the Authentication Logic
You can easily extend this pattern to support specific authentication schemes such as Bearer tokens, Basic Auth, or API keys. Simply parse the auth_str value accordingly. For example, you could check if the string starts with "Bearer " and validate the token that follows.
CORS Middleware
Cross-Origin Resource Sharing (CORS) is essential when your web API is consumed by JavaScript running on a different origin. Without proper CORS headers, browsers will block cross-origin requests.
Implementation
Hyperlane allows you to set CORS headers on the response using the standard response manipulation API. A CORS middleware typically sets three headers:
ctx.get_mut_response()
.set_header(ACCESS_CONTROL_ALLOW_ORIGIN, WILDCARD_ANY)
.set_header(ACCESS_CONTROL_ALLOW_METHODS, ALL_METHODS)
.set_header(ACCESS_CONTROL_ALLOW_HEADERS, WILDCARD_ANY);
Header Breakdown
| Header | Value | Meaning |
|---|---|---|
Access-Control-Allow-Origin |
* |
Any origin is allowed to access the resource |
Access-Control-Allow-Methods |
All HTTP methods | Permits GET, POST, PUT, DELETE, OPTIONS, etc. |
Access-Control-Allow-Headers |
* |
Any request header is allowed |
When to Apply CORS
Register this middleware as a request middleware so that CORS headers are set before any response is generated. This ensures that every response — whether it's a success, a redirect, or an error — includes the appropriate CORS headers.
For production environments, you may want to replace WILDCARD_ANY with specific allowed origins and headers to tighten security.
Static File Middleware
Serving static files (HTML, CSS, JavaScript, images) is a common requirement for web servers. Hyperlane provides the building blocks to implement a static file middleware.
Implementation
The middleware reads the requested path, resolves it to a file on disk, determines the content type from the file extension, reads the file contents, and sets them as the response body.
let file_path: String = format!("{static_path}{path}");
let file_extension: String = FileExtension::get_extension_name(&file_path);
let content_type: &'static str = FileExtension::parse(&file_extension).get_content_type();
let file_data: String = tokio::fs::read_to_string(&file_path).await.unwrap();
ctx.get_mut_response().set_body(file_data);
How It Works Step by Step
-
Path Construction: The
static_pathis a base directory (e.g.,./static/), andpathcomes from the request URI. They are concatenated to form the full file path. -
Extension Detection:
FileExtension::get_extension_nameextracts the file extension (e.g.,html,css,png). -
Content Type Resolution:
FileExtension::parsemaps the extension to its MIME type (e.g.,text/html,image/png). -
File Reading:
tokio::fs::read_to_stringasynchronously reads the entire file into a string. - Response Assembly: The file data is set as the response body.
Important Considerations
-
Security: Be careful with path traversal attacks. Ensure that the resolved
file_pathdoes not escape thestatic_pathdirectory. You should validate that the canonical path starts with the canonicalstatic_path. -
Error Handling: The example above uses
unwrap(), which will panic if the file doesn't exist. In production, handle the error gracefully by returning a404 Not Foundresponse. - Performance: For large files, consider streaming the file content instead of reading it all into memory at once.
Timeout Middleware
In asynchronous web servers, it's important to prevent individual requests from hanging indefinitely. A timeout middleware ensures that slow operations are aborted and the client receives a timely error response.
Implementation
Hyperlane leverages Tokio's timeout function to wrap potentially slow operations:
spawn(async move {
timeout(
Duration::from_millis(100),
async move {
new_ctx.get_mut_response()
.set_status_code(504)
.set_body("timeout");
}
).await.unwrap();
});
Understanding the Timeout Mechanism
-
Duration::from_millis(100)— Sets the timeout threshold to 100 milliseconds. Adjust this value based on your application's requirements. -
timeout(duration, future)— Wraps an async operation. If the operation doesn't complete within the specified duration, it returns anErr. -
spawn()— Runs the timeout-guarded operation as a separate Tokio task, so it doesn't block the main request processing loop.
When to Use Timeout Middleware
This pattern is particularly useful when:
- You call external services (databases, APIs) that might be slow or unresponsive.
- You perform computationally expensive operations.
- You want to enforce a strict SLA on response times.
If the timeout fires, the response status code is set to 504 Gateway Timeout and the body is set to "timeout", informing the client that the request took too long.
Combining Multiple Middlewares
In a real-world application, you'll typically use multiple middlewares together. Hyperlane supports this through the attribute macro system or programmatic registration:
#[request_middleware(1)]
struct RequestMiddleware1;
#[request_middleware(2)]
struct RequestMiddleware2;
#[response_middleware]
struct ResponseMiddleware;
When using #[request_middleware(N)], the numeric argument controls the execution order. Middlewares with lower numbers execute first, allowing you to establish a processing pipeline. For example:
- Middleware 1 (first): Timeout enforcement — ensures no request takes too long.
- Middleware 2 (second): Authentication — validates the user's credentials.
- Response Middleware (last): Response finalization — sends the built response to the client.
This composable architecture means each middleware has a single responsibility, making your codebase modular and testable.
Conclusion
Hyperlane's middleware system provides a flexible and powerful mechanism for cross-cutting concerns in web applications. We covered four essential patterns:
-
Authentication Middleware — Protects endpoints by validating the
Authorizationheader. - CORS Middleware — Enables cross-origin requests by setting the necessary headers.
- Static File Middleware — Serves files from the filesystem with automatic content type detection.
- Timeout Middleware — Guards against slow operations by enforcing a time limit.
Each pattern follows the same core principle: implement ServerHook, inspect or modify the request/response via Context, and return Status::Continue or Status::Reject. By combining these middlewares, you can build secure, performant, and feature-rich web services with minimal boilerplate.
Hyperlane's declarative attribute macros further reduce boilerplate, allowing you to focus on your application's unique business logic rather than infrastructure concerns. Experiment with these patterns and adapt them to your specific use cases.
Project Code:https://github.com/hyperlane-dev/hyperlane
Top comments (0)