Middleware System
Project Code:https://github.com/hyperlane-dev/hyperlane
Introduction
Middleware is one of the most powerful features of Hyperlane. It allows you to intercept and process requests and responses at various stages of the request lifecycle. Whether you need authentication, logging, CORS headers, or custom request transformation, Hyperlane's middleware system provides a clean and flexible way to implement these cross-cutting concerns.
Understanding the Middleware Architecture
Hyperlane's middleware system is built around the ServerHook trait. A middleware is any type that implements ServerHook, which provides two methods:
-
new(): Called when a new connection is established. Use this for initialization. -
handle(): Called for each request on the connection. This is where your middleware logic lives.
The handle() method returns a Status enum that controls the request flow:
-
Status::Continue: The request proceeds to the next middleware or route handler. -
Status::Reject: The request is rejected and the connection may be closed.
Request Middleware
Request middleware runs before the route handler processes the request. It's ideal for tasks like authentication, request validation, and setting up response defaults.
Here's a basic request middleware that sets default response headers:
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>();
In this example, the middleware:
- Creates a new instance when a connection is established (
new()) - Sets the HTTP version to 1.1 and status code to 200 for every request (
handle()) - Returns
Status::Continueto let the request proceed
Response Middleware
Response middleware runs after the route handler has processed the request. It's ideal for tasks like sending the response, adding response headers, and logging.
Here's a basic response middleware that sends the built response:
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>();
This middleware:
- Builds the response into bytes using
ctx.get_mut_response().build() - Sends the response data through the stream
- If sending fails, marks the stream as closed and rejects the request
Execution Order
The order in which middleware is registered matters. Request middleware runs in the order it's registered, and response middleware runs in reverse order. You can control the execution order using the attribute macro syntax:
#[request_middleware(1)]
struct RequestMiddleware1;
#[request_middleware(2)]
struct RequestMiddleware2;
#[response_middleware]
struct ResponseMiddleware;
The numeric parameter in #[request_middleware(N)] specifies the execution priority. Lower numbers run first. In this example:
-
RequestMiddleware1(priority 1) runs first -
RequestMiddleware2(priority 2) runs second - The route handler processes the request
-
ResponseMiddlewareruns last
Practical Middleware Examples
Authentication Middleware
One of the most common middleware use cases is authentication. Here's an example that checks for an Authorization header:
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
}
}
This middleware:
- Extracts the Authorization header from the request
- If the header is missing or empty, sends a 401 Unauthorized response
- Returns
Status::Rejectto stop further processing - If the header is present, returns
Status::Continueto proceed
CORS Middleware
Cross-Origin Resource Sharing (CORS) is essential for APIs that serve clients from different origins:
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);
This sets the standard CORS headers:
-
Access-Control-Allow-Origin: *— Allows requests from any origin -
Access-Control-Allow-Methods: *— Allows all HTTP methods -
Access-Control-Allow-Headers: *— Allows all request headers
Static Resource Middleware
For serving static files, you can create a middleware that reads files from disk:
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);
This middleware:
- Constructs the file path from a static directory and the request path
- Determines the file extension
- Maps the extension to a content type
- Reads the file contents and sets them as the response body
Timeout Middleware
Timeout middleware ensures that long-running requests don't block the server:
spawn(async move {
timeout(
Duration::from_millis(100),
async move {
new_ctx.get_mut_response()
.set_status_code(504)
.set_body("timeout");
}
).await.unwrap();
});
This spawns a task that waits for 100ms and then sets a 504 Gateway Timeout response.
Error Handling Middleware
Hyperlane provides specialized middleware for handling errors and panics:
Request Error Handler
struct RequestErrorHook;
impl ServerHook for RequestErrorHook {
async fn new(_: &mut Stream, ctx: &mut Context) -> Self {
let request_error: RequestError = ctx
.try_get_request_error_data()
.unwrap_or_default();
Self
}
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
let data: Vec<u8> = ctx
.get_mut_response()
.set_status_code(500)
.set_body("error")
.build();
stream.try_send(data).await;
Status::Continue
}
}
server.request_error::<RequestErrorHook>();
Task Panic Handler
struct TaskPanicHook;
impl ServerHook for TaskPanicHook {
async fn new(_: &mut Stream, ctx: &mut Context) -> Self {
let error: PanicData = ctx
.try_get_task_panic_data()
.unwrap_or_default();
Self
}
async fn handle(self, stream: &mut Stream, ctx: &mut Context) -> Status {
let data: Vec<u8> = ctx
.get_mut_response()
.set_status_code(500)
.set_body("panic")
.build();
stream.try_send(data).await;
Status::Continue
}
}
server.task_panic::<TaskPanicHook>();
These hooks catch errors and panics during request processing and return appropriate error responses instead of crashing the server.
Attribute Macro Middleware Registration
In addition to the programmatic API, you can register middleware using attribute macros:
#[request_middleware(1)]
struct RequestMiddleware1;
#[request_middleware(2)]
struct RequestMiddleware2;
#[response_middleware]
struct ResponseMiddleware;
This declarative approach is cleaner and more maintainable, especially when you have many middleware components. The macros handle the registration automatically.
Middleware Best Practices
Keep middleware focused: Each middleware should handle one concern. Don't try to do everything in a single middleware.
Order matters: Register middleware in the correct order. Authentication should come before authorization, which should come before logging.
Handle errors gracefully: Always handle the case where sending a response fails by checking the result of
stream.try_send().Use
Status::Rejectwisely: Only reject requests when you've already sent an appropriate error response.Leverage the
new()method: Use thenew()method for connection-level initialization rather than doing it inhandle()for every request.
Conclusion
Hyperlane's middleware system provides a powerful and flexible way to implement cross-cutting concerns in your web application. The ServerHook trait gives you full control over request and response processing, while the attribute macro system offers a more declarative approach. By combining request middleware, response middleware, and error handlers, you can build robust and maintainable web services.
In the next article, we'll explore Hyperlane's routing system, which determines how incoming requests are matched to their handlers.
Project Code:https://github.com/hyperlane-dev/hyperlane
Top comments (0)