tower-http Catalog

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against tower-http 0.7.1 with axum 0.8.9 on Rust 1.99 (edition 2024), checked on 2026-10-06. tower-http is the standard library of HTTP middleware for the tower ecosystem. Every module is behind a feature flag of the same name (trace, cors, compression-gzip, fs, csrf, …​); full enables all of them. Each section below gives the module’s purpose and a snippet; the detailed pages linked from each section show it in context. Where to place each layer is explained on Applying Middleware & Ordering.

[dependencies]
tower-http = { version = "0.7.1", features = ["trace", "request-id", "propagate-header", "sensitive-headers",
  "cors", "compression-gzip", "compression-br", "decompression-gzip", "timeout", "limit", "set-header",
  "catch-panic", "normalize-path", "validate-request", "auth", "csrf", "metrics", "fs", "util"] }

Observability: trace, request_id, propagate_header, sensitive_headers

  • trace — TraceLayer opens a tracing span per request and emits events on request, response, body chunks, end of stream and failure. new_for_http() classifies 5xx as failures; new_for_grpc() reads gRPC status codes. Its callbacks (make_span_with, on_response, on_failure) are customised on Logging & Tracing.

  • request_id — SetRequestIdLayer assigns an ID to requests that lack one (MakeRequestUuid, or your own MakeRequestId), and PropagateRequestIdLayer copies it to the response.

  • propagate_header — PropagateHeaderLayer copies any named header from the request to the response.

  • sensitive_headers — SetSensitiveRequestHeadersLayer / SetSensitiveResponseHeadersLayer mark headers such as Authorization and Set-Cookie as sensitive, so TraceLayer and Debug output show Sensitive instead of the value.

use axum::{
    Router,
    http::{HeaderName, header},
    routing::get,
};
use tower::ServiceBuilder;
use tower_http::{
    propagate_header::PropagateHeaderLayer,
    request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer},
    sensitive_headers::SetSensitiveHeadersLayer,
    trace::TraceLayer,
};

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt().with_max_level(tracing::Level::DEBUG).init();
    let request_id = HeaderName::from_static("x-request-id");
    let app = Router::new().route("/", get(|| async { "ok\n" })).layer(
        ServiceBuilder::new()
            .layer(SetSensitiveHeadersLayer::new([header::AUTHORIZATION, header::COOKIE, header::SET_COOKIE]))
            .layer(SetRequestIdLayer::new(request_id.clone(), MakeRequestUuid))
            .layer(TraceLayer::new_for_http())
            .layer(PropagateRequestIdLayer::new(request_id))
            .layer(PropagateHeaderLayer::new(HeaderName::from_static("x-correlation-id"))),
    );
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Cross-origin and encoding: cors, compression, decompression

  • cors — CorsLayer answers preflight requests and adds Access-Control-* headers; CorsLayer::permissive() and very_permissive() exist for development only.

  • compression — CompressionLayer compresses response bodies with gzip, deflate, brotli or zstd according to Accept-Encoding (each algorithm is a feature, e.g. compression-br), skipping small bodies, images and SSE by default; its predicates are configurable.

  • decompression — RequestDecompressionLayer decompresses request bodies sent with Content-Encoding; DecompressionLayer does the same for responses in HTTP clients.

Both are covered in detail, with the CORS pitfalls, on CORS & Compression.

Time and size limits: timeout, limit

  • timeout — TimeoutLayer::with_status_code(status, duration) responds with the given status when the handler takes too long (TimeoutLayer::new is deprecated since 0.6.7). RequestBodyTimeoutLayer and ResponseBodyTimeoutLayer limit the idle time between body frames. New in 0.7, RequestBodyDeadlineLayer and ResponseBodyDeadlineLayer cap the total body transfer time, which stops slow clients that trickle a byte at a time and never trip an idle timeout.

  • limit — RequestBodyLimitLayer rejects bodies above a size with 413, checking Content-Length up front and counting streamed bytes.

use std::time::Duration;

use axum::{Router, http::StatusCode, routing::post};
use tower::ServiceBuilder;
use tower_http::{
    limit::RequestBodyLimitLayer,
    timeout::{RequestBodyDeadlineLayer, TimeoutLayer},
};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/api/v1/books", post(|body: String| async move { format!("{} bytes\n", body.len()) })).layer(
        ServiceBuilder::new()
            .layer(TimeoutLayer::with_status_code(StatusCode::REQUEST_TIMEOUT, Duration::from_secs(10)))
            .layer(RequestBodyDeadlineLayer::new(Duration::from_secs(30)))
            .layer(RequestBodyLimitLayer::new(64 * 1024)),
    );
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

axum’s own DefaultBodyLimit interacts with RequestBodyLimitLayer; the rules are on Request Limits & Timeouts.

Headers and robustness: set_header, set_status, catch_panic, normalize_path

  • set_header — SetResponseHeaderLayer::overriding, appending or if_not_present set a response header (a fixed value or a closure of the response); SetRequestHeaderLayer does the same for requests. Bookshelf uses them for security headers (see CSRF & Security Hardening).

  • set_status — SetStatusLayer forces a status code, which is how ServeDir::not_found_service returns 404.

  • catch_panic — CatchPanicLayer turns a panic into a 500 (see Error Handling).

  • normalize_path — NormalizePathLayer::trim_trailing_slash() / append_trailing_slash(); it must wrap the router from outside (see Routing).

use axum::{
    Router,
    http::{HeaderValue, header},
    routing::get,
};
use tower::ServiceBuilder;
use tower_http::{catch_panic::CatchPanicLayer, set_header::SetResponseHeaderLayer};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(|| async { "ok\n" })).layer(
        ServiceBuilder::new()
            .layer(CatchPanicLayer::new())
            .layer(SetResponseHeaderLayer::overriding(header::X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff")))
            .layer(SetResponseHeaderLayer::if_not_present(header::CACHE_CONTROL, HeaderValue::from_static("no-store"))),
    );
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Request validation and authorisation: validate_request, auth

  • validate_request — ValidateRequestHeaderLayer::accept("application/json") rejects requests whose Accept does not allow a type (406); has_header_value(name, value) (new in 0.7) requires an exact header value; custom takes any validator. The fixed-credential helpers ValidateRequestHeaderLayer::bearer(token) and basic(user, pass) are deprecated since 0.6.7 ("too basic to be useful in real applications"); for an internal endpoint such as a metrics scraper, has_header_value("authorization", ...) or a custom validator does the same job.

  • auth — AsyncRequireAuthorizationLayer runs an async authoriser that may modify the request (insert the user into extensions) or reject it; AddAuthorizationLayer adds an Authorization header to outgoing client requests.

use axum::{Router, routing::get};
use tower_http::validate_request::ValidateRequestHeaderLayer;

#[tokio::main]
async fn main() {
    // The scrape token comes from the environment (or a secret store); no default, so a missing
    // token stops the process at start-up instead of protecting /metrics with a guessable value.
    let token = std::env::var("METRICS_TOKEN").expect("METRICS_TOKEN must be set");
    let internal = Router::new()
        .route("/metrics", get(|| async { "# HELP ...\n" }))
        .layer(
            ValidateRequestHeaderLayer::has_header_value("authorization", &format!("Bearer {token}"))
                .expect("valid header name"),
        );
    let app = Router::new()
        .route("/api/v1/books", get(|| async { "[]" }).layer(ValidateRequestHeaderLayer::accept("application/json")))
        .merge(internal);
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

csrf (new in 0.7)

CsrfLayer implements the cross-origin protection scheme that Go 1.25 introduced: no tokens, only the browser-supplied Sec-Fetch-Site and Origin headers. Safe methods (GET, HEAD, OPTIONS) always pass; an unsafe request passes if its Origin is on the trusted list, if Sec-Fetch-Site is same-origin or none, if neither header is present (non-browser clients), or if the Origin authority equals the request’s Host. Everything else gets 403 Forbidden.

use axum::{Router, routing::post};
use tower_http::csrf::CsrfLayer;

#[tokio::main]
async fn main() {
    let csrf = CsrfLayer::new().add_trusted_origin("https://bookshelf.example").expect("valid origin");
    let app = Router::new().route("/account/delete", post(|| async { "deleted\n" })).layer(csrf);
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
$ curl -si -X POST -H 'sec-fetch-site: cross-site' -H 'origin: https://evil.example' localhost:3000/account/delete | head -1
HTTP/1.1 403 Forbidden
$ curl -si -X POST -H 'sec-fetch-site: same-origin' localhost:3000/account/delete | head -1
HTTP/1.1 200 OK

The protection depends on a reverse proxy forwarding Origin and Host unchanged. When and how to combine it with tokens is discussed on CSRF & Security Hardening.

metrics

InFlightRequestsLayer::pair() returns a layer and an InFlightRequestsCounter; the counter’s get() (or its run_emitter helper) reports how many requests are being processed — a saturation signal for autoscaling and load shedding (see Metrics & SLOs).

use std::time::Duration;

use axum::{Router, routing::get};
use tower_http::metrics::InFlightRequestsLayer;

#[tokio::main]
async fn main() {
    let (in_flight, counter) = InFlightRequestsLayer::pair();
    tokio::spawn(counter.run_emitter(Duration::from_secs(10), |n| async move {
        tracing::info!(in_flight = n, "requests in flight");
    }));
    let app = Router::new().route("/", get(|| async { "ok\n" })).layer(in_flight);
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

services::fs: ServeDir and ServeFile

ServeDir serves a directory and ServeFile a single file, with Content-Type from the extension, range requests, Last-Modified, precompressed .gz/.br/.zst variants and — new in 0.7 — strong ETags with If-Match/ If-None-Match handling and a pluggable Backend trait for embedded or remote assets. They are services, mounted with nest_service or fallback_service; see Static Files & SPAs.

follow_redirect and other client-side layers

Some tower-http layers are for HTTP clients built on tower (hyper-util’s legacy client, or reqwest through adapters): FollowRedirectLayer (in 0.7 it forwards request extensions on redirects and filters credentials cumulatively across hops), DecompressionLayer and AddAuthorizationLayer. A gateway that proxies to upstream services uses them; see HTTP Clients & Proxies.

Changes from 0.6 to 0.7

Area Change in 0.7.0 / 0.7.1

new

csrf (CsrfLayer); body deadline layers; strong ETags, Backend trait, html_as_default_extension and redirect_path_prefix on ServeDir; ValidateRequestHeaderLayer::has_header_value; ServeDir::redirect_to_trailing_slash(false) (0.7.1)

breaking: compression

Accept-Encoding: * and identity;q=0 handled per RFC 9110, which can now produce 406; SizeAbove takes a u64

breaking: features

the no-op tokio and async-compression features were removed; delete them from Cargo.toml

breaking: fs

a request for a file with a trailing slash now returns 404; since 0.7.1, unexpected I/O errors in ServeDir::try_call propagate instead of becoming 404

breaking: trace

gRPC error messages included; GrpcCode/GrpcFailureClass are #[non_exhaustive]

breaking: follow_redirect

extensions forwarded by default; header filtering is cumulative

other

cors relaxed its Vary defaults; MSRV 1.65

TimeoutLayer::new was already deprecated in 0.6.7, so code written for 0.6 should have moved to with_status_code. Libraries that pin tower-http 0.6 — tower_governor 0.8 and leptos_axum 0.8 at the time of writing — pull a second copy of tower-http into the build. That compiles and works (the layers are independent types), but costs compile time, and you cannot share tower-http types between the two versions.