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"] }
Source: tower-http: Feature flags
Observability: trace, request_id, propagate_header, sensitive_headers
-
trace—TraceLayeropens atracingspan per request and emits events on request, response, body chunks, end of stream and failure.new_for_http()classifies5xxas failures;new_for_grpc()reads gRPC status codes. Its callbacks (make_span_with,on_response,on_failure) are customised on Logging & Tracing. -
request_id—SetRequestIdLayerassigns an ID to requests that lack one (MakeRequestUuid, or your ownMakeRequestId), andPropagateRequestIdLayercopies it to the response. -
propagate_header—PropagateHeaderLayercopies any named header from the request to the response. -
sensitive_headers—SetSensitiveRequestHeadersLayer/SetSensitiveResponseHeadersLayermark headers such asAuthorizationandSet-Cookieas sensitive, soTraceLayerandDebugoutput showSensitiveinstead 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();
}
Source: tower-http: trace and request_id
Cross-origin and encoding: cors, compression, decompression
-
cors—CorsLayeranswers preflight requests and addsAccess-Control-*headers;CorsLayer::permissive()andvery_permissive()exist for development only. -
compression—CompressionLayercompresses response bodies with gzip, deflate, brotli or zstd according toAccept-Encoding(each algorithm is a feature, e.g.compression-br), skipping small bodies, images and SSE by default; its predicates are configurable. -
decompression—RequestDecompressionLayerdecompresses request bodies sent withContent-Encoding;DecompressionLayerdoes 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::newis deprecated since 0.6.7).RequestBodyTimeoutLayerandResponseBodyTimeoutLayerlimit the idle time between body frames. New in 0.7,RequestBodyDeadlineLayerandResponseBodyDeadlineLayercap the total body transfer time, which stops slow clients that trickle a byte at a time and never trip an idle timeout. -
limit—RequestBodyLimitLayerrejects bodies above a size with413, checkingContent-Lengthup 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();
}
Source: tower-http: timeout and limit
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,appendingorif_not_presentset a response header (a fixed value or a closure of the response);SetRequestHeaderLayerdoes the same for requests. Bookshelf uses them for security headers (see CSRF & Security Hardening). -
set_status—SetStatusLayerforces a status code, which is howServeDir::not_found_servicereturns404. -
catch_panic—CatchPanicLayerturns a panic into a500(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();
}
Source: tower-http: set_header
Request validation and authorisation: validate_request, auth
-
validate_request—ValidateRequestHeaderLayer::accept("application/json")rejects requests whoseAcceptdoes not allow a type (406);has_header_value(name, value)(new in 0.7) requires an exact header value;customtakes any validator. The fixed-credential helpersValidateRequestHeaderLayer::bearer(token)andbasic(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 acustomvalidator does the same job. -
auth—AsyncRequireAuthorizationLayerruns an async authoriser that may modify the request (insert the user into extensions) or reject it;AddAuthorizationLayeradds anAuthorizationheader 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();
}
Source: tower-http: validate_request and auth
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();
}
Source: tower-http: csrf
$ 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
Source: tower-http: csrf and MDN: Sec-Fetch-Site
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();
}
Source: tower-http: in_flight_requests
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 |
|
breaking: compression |
|
breaking: features |
the no-op |
breaking: fs |
a request for a file with a trailing slash now returns |
breaking: trace |
gRPC error messages included; |
breaking: follow_redirect |
extensions forwarded by default; header filtering is cumulative |
other |
|
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.
Source: tower-http CHANGELOG