DEV Community

tengxgfyrz67s
tengxgfyrz67s

Posted on

Built-in Middleware Patterns

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

  1. Overview of the Middleware System
  2. Authentication Middleware
  3. CORS Middleware
  4. Static File Middleware
  5. Timeout Middleware
  6. Combining Multiple Middlewares
  7. 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>();
Enter fullscreen mode Exit fullscreen mode

You can also use attribute macros for a declarative approach:

#[request_middleware(1)]
struct RequestMiddleware1;

#[request_middleware(2)]
struct RequestMiddleware2;

#[response_middleware]
struct ResponseMiddleware;
Enter fullscreen mode Exit fullscreen mode

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

Key Details

  • try_get_header_back(AUTHORIZATION) — Attempts to retrieve the Authorization header. Returns None if the header is absent.
  • unwrap_or_default() — Converts None to 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);
Enter fullscreen mode Exit fullscreen mode

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

How It Works Step by Step

  1. Path Construction: The static_path is a base directory (e.g., ./static/), and path comes from the request URI. They are concatenated to form the full file path.
  2. Extension Detection: FileExtension::get_extension_name extracts the file extension (e.g., html, css, png).
  3. Content Type Resolution: FileExtension::parse maps the extension to its MIME type (e.g., text/html, image/png).
  4. File Reading: tokio::fs::read_to_string asynchronously reads the entire file into a string.
  5. 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_path does not escape the static_path directory. You should validate that the canonical path starts with the canonical static_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 a 404 Not Found response.
  • 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();
});
Enter fullscreen mode Exit fullscreen mode

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 an Err.
  • 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;
Enter fullscreen mode Exit fullscreen mode

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:

  1. Middleware 1 (first): Timeout enforcement — ensures no request takes too long.
  2. Middleware 2 (second): Authentication — validates the user's credentials.
  3. 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 Authorization header.
  • 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)