DEV Community

tengxgfyrz67s
tengxgfyrz67s

Posted on

Middleware-System

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:

  1. new(): Called when a new connection is established. Use this for initialization.
  2. 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>();
Enter fullscreen mode Exit fullscreen mode

In this example, the middleware:

  1. Creates a new instance when a connection is established (new())
  2. Sets the HTTP version to 1.1 and status code to 200 for every request (handle())
  3. Returns Status::Continue to 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>();
Enter fullscreen mode Exit fullscreen mode

This middleware:

  1. Builds the response into bytes using ctx.get_mut_response().build()
  2. Sends the response data through the stream
  3. 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;
Enter fullscreen mode Exit fullscreen mode

The numeric parameter in #[request_middleware(N)] specifies the execution priority. Lower numbers run first. In this example:

  1. RequestMiddleware1 (priority 1) runs first
  2. RequestMiddleware2 (priority 2) runs second
  3. The route handler processes the request
  4. ResponseMiddleware runs 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
    }
}
Enter fullscreen mode Exit fullscreen mode

This middleware:

  1. Extracts the Authorization header from the request
  2. If the header is missing or empty, sends a 401 Unauthorized response
  3. Returns Status::Reject to stop further processing
  4. If the header is present, returns Status::Continue to 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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

This middleware:

  1. Constructs the file path from a static directory and the request path
  2. Determines the file extension
  3. Maps the extension to a content type
  4. 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();
});
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

This declarative approach is cleaner and more maintainable, especially when you have many middleware components. The macros handle the registration automatically.

Middleware Best Practices

  1. Keep middleware focused: Each middleware should handle one concern. Don't try to do everything in a single middleware.

  2. Order matters: Register middleware in the correct order. Authentication should come before authorization, which should come before logging.

  3. Handle errors gracefully: Always handle the case where sending a response fails by checking the result of stream.try_send().

  4. Use Status::Reject wisely: Only reject requests when you've already sent an appropriate error response.

  5. Leverage the new() method: Use the new() method for connection-level initialization rather than doing it in handle() 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)