You are reading the docs for rws 18.2 The latest release is 18.8. Read this page for the latest release.

Middleware

Add cross-cutting behaviour to any Application by implementing the Middleware trait and stacking layers with .wrap().

The Middleware trait

Middleware is defined in src/middleware/mod.rs:

pub trait Middleware: Send + Sync {
    fn handle(
        &self,
        request: &Request,
        connection: &ConnectionInfo,
        next: &dyn Application,
    ) -> Result<Response, String>;
}

Call next.execute(request, connection) to pass the request to the next layer (or the inner application). Return a Response directly to short-circuit the chain.

Wrapping an application

.wrap(layer) attaches a middleware layer to any Application. Layers run in registration order — the first .wrap() call is the outermost layer (runs first on the way in, last on the way out):

use rust_web_server::app::App;
use rust_web_server::middleware::{RateLimitLayer, WithMiddleware};
use rust_web_server::core::New;

let app = App::new()
    .wrap(RateLimitLayer);  // outermost: checked before any route

Chain multiple layers:

use rust_web_server::metrics::MetricsLayer;

let app = App::new()
    .wrap(RateLimitLayer)  // checked first
    .wrap(MetricsLayer);   // checked second

App, App::with_state(S) and an already-wrapped WithMiddleware<A> expose the same .wrap() method; for any other Application (e.g. AsyncAppWithState), start with WithMiddleware::new(app).wrap(layer).

Built-in middleware layers

Layer Module Description
RateLimitLayer middleware Enforces the process-wide sliding-window rate limit; returns 429 when the budget for the client IP is exceeded. Configured via RWS_CONFIG_RATE_LIMIT_MAX_REQUESTS / RWS_CONFIG_RATE_LIMIT_WINDOW_SECS.
MetricsLayer metrics Records per-route rws_route_requests_total counters and rws_route_duration_seconds histograms in Prometheus format.
CacheLayer cache In-process response cache; caches GET responses by URI.
OtelLayer otel Emits OpenTelemetry spans and propagates W3C traceparent context.
RewriteLayer rewrite Applies configurable request/response rewrite rules (header set/remove, URI prefix manipulation, response body find-and-replace).
ReverseProxy proxy HTTP/1.1 reverse proxy; forwards the request to a backend and returns its response.
H2ReverseProxy proxy HTTP/2 upstream proxy (requires http2 feature).
GrpcProxy proxy Wraps H2ReverseProxy; only proxies requests with Content-Type: application/grpc*.
BasicAuthLayer auth HTTP Basic authentication; calls a user-supplied verifier closure.
JwtLayer auth JWT Bearer token verification; rejects requests without a valid token.
IpFilter ip_filter Allow-list or deny-list based on client IP address or CIDR range.
BlocklistLayer blocklist Runtime-updatable IP blocklist; unlike IpFilter, the list can be modified without restart.
CanaryLayer canary Routes a configured percentage of traffic to a canary backend.
RetryLayer circuit_breaker Re-dispatches a request whose response status is in the retry_on list, up to max_retries more times (no backoff).
CsrfLayer csrf CSRF token validation for state-mutating requests.
LogLayer request_log Structured per-request access logging (combined or JSON format).
MaintenanceLayer maintenance Returns 503 Service Unavailable when maintenance mode is active.
RequestIdLayer request_id Reads or generates a request/correlation ID and echoes it on the request and response (X-Request-Id by default).
ForwardAuthLayer auth::forward Delegates the allow/deny decision for every request to an external HTTP auth service.

Quick usage examples

use rust_web_server::app::App;
use rust_web_server::core::New;
use rust_web_server::middleware::RateLimitLayer;
use rust_web_server::metrics::MetricsLayer;
use rust_web_server::ip_filter::IpFilter;
use rust_web_server::auth::JwtLayer;
use rust_web_server::request_log::LogLayer;

let app = App::new()
    .wrap(LogLayer)
    .wrap(IpFilter::deny(["1.2.3.4", "10.0.0.0/8"]))
    .wrap(JwtLayer::new(b"secret".to_vec()))
    .wrap(RateLimitLayer)
    .wrap(MetricsLayer);

Writing a custom middleware layer

Implement the Middleware trait on a unit struct (or a struct carrying configuration):

use rust_web_server::middleware::Middleware;
use rust_web_server::application::Application;
use rust_web_server::request::Request;
use rust_web_server::response::Response;
use rust_web_server::server::ConnectionInfo;

/// Adds an X-Request-Id header to every response.
pub struct RequestIdLayer;

impl Middleware for RequestIdLayer {
    fn handle(
        &self,
        request: &Request,
        connection: &ConnectionInfo,
        next: &dyn Application,
    ) -> Result<Response, String> {
        // Call into the rest of the chain / inner application
        let mut response = next.execute(request, connection)?;

        // Mutate the response on the way out
        let id = format!("{}-{}", connection.server.ip, connection.server.port);
        response.headers.push(rust_web_server::header::Header::new("X-Request-Id", id));

        Ok(response)
    }
}

Register it like any built-in layer:

use rust_web_server::app::App;
use rust_web_server::core::New;

let app = App::new().wrap(RequestIdLayer);

Running on the event loop (runs_inline)

The async servers normally hand each request to a blocking thread pool, so a slow handler can't stall other connections. For requests that are quick and never wait — health checks, metrics, small static files — that handoff costs more than the work, so rws runs them directly on the event loop, as nginx does. A request runs inline only if every layer and the inner application say so for that request:

impl Middleware for RequestIdLayer {
    fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application)
        -> Result<Response, String> { /* … */ }

    // Only in-memory work: safe on the event loop.
    fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
        true
    }
}

The default is false, and it must stay false for a layer that can wait — a database or Redis call, an outbound HTTP request, a lock held across I/O. A layer that doesn't opt in simply keeps the stack on the blocking pool, so leaving it out is always safe. The built-in in-memory layers (RateLimitLayer, CacheLayer, IpFilter, RequestIdLayer, MetricsLayer, LogLayer, BlocklistLayer, MaintenanceLayer) opt in; user route handlers, proxy routes and MCP calls never run inline.

Streamed request bodies (accepts_streamed_body)

An application can take a request's body as a stream (Application::streams_body, see Streaming large uploads). Through a WithMiddleware stack that happens only if every layer returns true from accepts_streamed_body — the layer then sees an empty request.body, so return true only from a layer that never reads it. The built-in header-only layers do; the default is false.

Passing requests through (is_passthrough)

Some applications can answer a request asynchronously (Application::execute_async, http2 feature) — the config-driven proxy does, for plain-HTTP routes without middleware, so it waits for the backend without holding a thread. A middleware stack can only offer that path if none of its layers needs to run. A layer that does nothing for a given request — a rate limit that isn't configured, a filter that doesn't apply — says so with is_passthrough:

fn is_passthrough(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
    !self.enabled
}

Default false: the request then takes the usual synchronous path through every layer.

Short-circuit example

Return a response without calling next to block the request:

use rust_web_server::middleware::Middleware;
use rust_web_server::application::Application;
use rust_web_server::error::{AppError, IntoResponse};
use rust_web_server::request::{METHOD, Request};
use rust_web_server::response::Response;
use rust_web_server::server::ConnectionInfo;

/// Rejects all non-GET/HEAD requests to /public/*.
pub struct ReadOnlyPublicLayer;

impl Middleware for ReadOnlyPublicLayer {
    fn handle(
        &self,
        request: &Request,
        connection: &ConnectionInfo,
        next: &dyn Application,
    ) -> Result<Response, String> {
        let is_write = request.method != METHOD.get && request.method != METHOD.head;
        let is_public = request.request_uri.starts_with("/public/");

        if is_write && is_public {
            return Ok(AppError::Forbidden.into_response());
        }

        next.execute(request, connection)
    }
}
Layer ordering

Layers push in order: app.wrap(A).wrap(B) runs A before B on the request path and B before A on the response path. Put logging and tracing outermost, authentication next, and business-logic concerns innermost.

Edit this page on GitHub