You are reading the docs for rws 18.6 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.

A layer that answers a request itself — a proxy forwarding it, without calling next — can claim the stream with answers_streamed_body (since 18.3.0): the body then streams whatever the layers after it and the application would say, provided the layers before it accept it. H2ReverseProxy and GrpcProxy do, for the requests they forward (gRPC client streaming).

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