Writing Middleware
|
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 axum 0.8.9 on Rust 1.99 (edition 2024) with tower 0.5.3 and tower-http 0.7.1, checked on
2026-10-06. axum has no middleware system of its own; middleware is any tower Layer (see
Tower Service Model). Writing a Layer by hand is verbose, so axum offers
adapters that turn async functions into layers. Choosing where to apply middleware and in which order is covered on
Applying Middleware & Ordering, and ready-made layers on
tower-http Catalog.
Where it runs in the request
In the request lifecycle (tokio accepts
the connection, hyper parses the request, tower layers wrap the router, the router matches a route, middleware and
extractors run, the handler runs, the response travels back out), middleware written on this page runs between the
router and the extractors when added with Router::layer or route_layer, and between hyper and the router when
it wraps the whole Router as a tower layer. Either way it sees the request before the handler’s extractors and the
response after IntoResponse, and it can short-circuit with its own response without calling the handler.
Which tool?
| Tool | Use when |
|---|---|
|
the default: an async function that sees the request, may short-circuit, calls the rest of the stack and sees the response |
|
you only transform the request or the response |
|
an existing extractor already encodes the check (authentication) |
tower combinators ( |
trivial synchronous transformations |
a hand-written |
you are publishing reusable middleware, need per-connection state, back-pressure
( |
Source: axum: Writing middleware
from_fn
A from_fn middleware is an async function whose last two arguments are the Request and a Next; any number of
extractors may come first (they must implement FromRequestParts). It returns something that implements
IntoResponse:
use std::time::Instant;
use axum::{
Router,
extract::{MatchedPath, Request},
http::{HeaderValue, StatusCode},
middleware::{self, Next},
response::{IntoResponse, Response},
routing::get,
};
/// Adds `server-timing` and rejects requests without an Accept header.
async fn timing(matched: Option<MatchedPath>, req: Request, next: Next) -> Result<Response, StatusCode> {
if !req.headers().contains_key("accept") {
return Err(StatusCode::NOT_ACCEPTABLE); // short-circuit: the handler never runs
}
let start = Instant::now();
let mut response = next.run(req).await; // call the rest of the stack
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
let route = matched.as_ref().map_or("unmatched", MatchedPath::as_str);
let value = format!("app;dur={elapsed:.2};desc=\"{route}\"");
if let Ok(v) = HeaderValue::from_str(&value) {
response.headers_mut().insert("server-timing", v);
}
Ok(response)
}
async fn books() -> impl IntoResponse {
"books\n"
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/api/v1/books", get(books)).layer(middleware::from_fn(timing));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Source: axum::middleware::from_fn
$ curl -si localhost:3000/api/v1/books | grep server-timing
server-timing: app;dur=0.04;desc="/api/v1/books"
$ curl -si -H 'accept:' localhost:3000/api/v1/books | head -1
HTTP/1.1 406 Not Acceptable
Source: axum::middleware::from_fn
-
next.run(req)returns aResponse; there is no error to handle, because everything below is infallible. -
Code before
next.runsees the request, code after it sees the response. Not callingnext.runshort-circuits. -
MatchedPathisOptionbecause aRouter::layermiddleware also runs for unmatched requests (the fallback). -
The middleware function must satisfy the same rules as a handler:
Clone + Send + Sync + 'static, aSendfuture.
Accessing state: from_fn_with_state
use std::sync::Arc;
use axum::{
Router,
extract::{Request, State},
http::StatusCode,
middleware::{self, Next},
response::Response,
routing::get,
};
#[derive(Clone)]
struct AppState {
maintenance: Arc<std::sync::atomic::AtomicBool>,
}
async fn maintenance_mode(State(state): State<AppState>, req: Request, next: Next) -> Result<Response, StatusCode> {
if state.maintenance.load(std::sync::atomic::Ordering::Relaxed) && req.method() != "GET" {
return Err(StatusCode::SERVICE_UNAVAILABLE); // read-only while maintenance runs
}
Ok(next.run(req).await)
}
#[tokio::main]
async fn main() {
let state = AppState { maintenance: Arc::new(true.into()) };
let app = Router::new()
.route("/api/v1/books", get(|| async { "books\n" }).post(|| async { "created\n" }))
.layer(middleware::from_fn_with_state(state.clone(), maintenance_mode))
.with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
The state is passed twice: once to the layer (it is created before the router knows its state) and once to the
router. Forgetting the first produces a "trait bound FromFn<...>: Service<...> is not satisfied" error;
#[axum::debug_middleware] on the function turns that into a precise message, like #[debug_handler] does for
handlers.
Passing data to handlers: request extensions
Middleware often computes something the handler needs — the authenticated user, a request ID, a tenant. Insert it into
the request’s extensions (a type map) and extract it in the handler with Extension<T>:
use axum::{
Extension, Router,
extract::Request,
http::StatusCode,
middleware::{self, Next},
response::Response,
routing::get,
};
#[derive(Clone, Debug)]
struct Member {
id: u64,
name: String,
}
async fn authenticate(mut req: Request, next: Next) -> Result<Response, StatusCode> {
let token = req
.headers()
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.ok_or(StatusCode::UNAUTHORIZED)?;
// a real implementation verifies a session or JWT here
let member = match token {
"demo-token-not-a-secret" => Member { id: 42, name: "Octavia".into() },
_ => return Err(StatusCode::UNAUTHORIZED),
};
req.extensions_mut().insert(member);
Ok(next.run(req).await)
}
async fn my_loans(Extension(member): Extension<Member>) -> String {
format!("loans of member {} ({})\n", member.id, member.name)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/api/v1/me/loans", get(my_loans))
.route_layer(middleware::from_fn(authenticate)); // only for matched routes
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
If a handler extracts Extension<Member> on a route the middleware does not cover, the request fails at runtime with a
500 ("Missing request extension"). A custom CurrentUser extractor (see
Custom Extractors) avoids that failure mode and is how Bookshelf does it;
extensions remain the right tool for data that only middleware can compute.
map_request, map_response and from_extractor
use axum::{
Router,
extract::{FromRequestParts, Request},
http::{HeaderValue, StatusCode, request::Parts},
middleware::{from_extractor, map_request, map_response},
response::Response,
routing::get,
};
async fn add_request_header(mut req: Request) -> Request {
req.headers_mut().insert("x-bookshelf-edge", HeaderValue::from_static("eu-west"));
req
}
async fn add_security_headers(mut res: Response) -> Response {
res.headers_mut().insert("x-content-type-options", HeaderValue::from_static("nosniff"));
res
}
/// An extractor used as a guard: requests without the API key never reach the handlers.
struct ApiKey;
impl<S: Send + Sync> FromRequestParts<S> for ApiKey {
type Rejection = StatusCode;
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
let expected = std::env::var("BOOKSHELF_ADMIN_KEY").map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
match parts.headers.get("x-api-key") {
Some(k) if k.as_bytes() == expected.as_bytes() => Ok(ApiKey),
_ => Err(StatusCode::UNAUTHORIZED),
}
}
}
#[tokio::main]
async fn main() {
let admin = Router::new()
.route("/reindex", get(|| async { "reindexing\n" }))
.route_layer(from_extractor::<ApiKey>());
let app = Router::new()
.route("/", get(|req: Request| async move { format!("{:?}\n", req.headers().get("x-bookshelf-edge")) }))
.nest("/admin", admin)
.layer(map_request(add_request_header))
.layer(map_response(add_security_headers));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Source: axum::middleware::map_request, map_response and from_extractor
map_request functions may also return Result<Request, E> to reject, and both accept extractors before the request
or response argument. Comparing secrets with == is fine for an admin demo; for real API keys, compare hashes or use a
constant-time comparison (see Password Authentication).
Rewriting the request URI
Middleware added with Router::layer runs after routing, so changing the URI there does not change which route
matches. To rewrite before routing, wrap the whole router — as with NormalizePathLayer on
Routing:
use axum::{Router, ServiceExt, extract::Request, http::Uri, routing::get};
use tower::Layer;
fn rewrite_legacy_paths(mut req: Request) -> Request {
// /catalog/... (old site) -> /catalogue/...
if let Some(rest) = req.uri().path().strip_prefix("/catalog/") {
let new = format!("/catalogue/{rest}");
if let Ok(uri) = new.parse::<Uri>() {
*req.uri_mut() = uri;
}
}
req
}
#[tokio::main]
async fn main() {
let router = Router::new().route("/catalogue/{shelf}", get(|uri: Uri| async move { format!("{uri}\n") }));
let middleware = tower::util::MapRequestLayer::new(rewrite_legacy_paths);
let app = middleware.layer(router);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, ServiceExt::<Request>::into_make_service(app)).await.unwrap();
}
This rewrite drops the query string; a production version keeps uri.query(). For SEO-relevant moves, a 308
redirect is usually better than a silent rewrite.
Reading and restoring the body
Middleware that must inspect the body — signature verification, audit logging — has to buffer it and put it back,
because a body can be read only once. The official print-request-response example does it like this:
use axum::{
Router,
body::{Body, Bytes},
extract::Request,
http::StatusCode,
middleware::{self, Next},
response::{IntoResponse, Response},
routing::post,
};
use http_body_util::BodyExt;
async fn print_request_response(req: Request, next: Next) -> Result<impl IntoResponse, (StatusCode, String)> {
let (parts, body) = req.into_parts();
let bytes = buffer_and_print("request", body).await?;
let req = Request::from_parts(parts, Body::from(bytes));
let res = next.run(req).await;
let (parts, body) = res.into_parts();
let bytes = buffer_and_print("response", body).await?;
Ok(Response::from_parts(parts, Body::from(bytes)))
}
async fn buffer_and_print<B>(direction: &str, body: B) -> Result<Bytes, (StatusCode, String)>
where
B: axum::body::HttpBody<Data = Bytes>,
B::Error: std::fmt::Display,
{
let bytes = match body.collect().await {
Ok(collected) => collected.to_bytes(),
Err(err) => return Err((StatusCode::BAD_REQUEST, format!("failed to read {direction} body: {err}"))),
};
if let Ok(body) = std::str::from_utf8(&bytes) {
tracing::debug!("{direction} body = {body:?}");
}
Ok(bytes)
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt().with_env_filter("debug").init();
let app = Router::new()
.route("/api/v1/reviews", post(|body: String| async move { format!("stored {} bytes\n", body.len()) }))
.layer(middleware::from_fn(print_request_response));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Buffering defeats streaming and costs memory, so apply such middleware with route_layer to the few routes that need
it, and put a body limit in front (see Request Limits & Timeouts).
The consume-body-in-extractor-or-middleware example shows the same technique inside an extractor. Never log bodies
that may contain passwords or tokens.
A hand-written Service and Layer
For reusable middleware — something you would publish as a crate — implement tower’s traits directly. A Layer is a
factory that wraps an inner service; the Service does the work. With Pin<Box<dyn Future>> as the future type the
code stays readable:
use std::task::{Context, Poll};
use std::time::Duration;
use axum::{Router, extract::Request, response::Response, routing::get};
use futures::future::BoxFuture;
use tower::{Layer, Service};
/// Logs requests slower than a threshold.
#[derive(Clone)]
struct SlowRequestLayer {
threshold: Duration,
}
impl<S> Layer<S> for SlowRequestLayer {
type Service = SlowRequest<S>;
fn layer(&self, inner: S) -> Self::Service {
SlowRequest { inner, threshold: self.threshold }
}
}
#[derive(Clone)]
struct SlowRequest<S> {
inner: S,
threshold: Duration,
}
impl<S> Service<Request> for SlowRequest<S>
where
S: Service<Request, Response = Response> + Send + 'static,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx) // propagate the inner service's readiness
}
fn call(&mut self, req: Request) -> Self::Future {
let path = req.uri().path().to_owned();
let threshold = self.threshold;
let start = std::time::Instant::now();
let future = self.inner.call(req);
Box::pin(async move {
let response = future.await?;
if start.elapsed() > threshold {
tracing::warn!(%path, elapsed_ms = start.elapsed().as_millis(), "slow request");
}
Ok(response)
})
}
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt().with_max_level(tracing::Level::INFO).init();
let app = Router::new()
.route("/slow", get(|| async {
tokio::time::sleep(Duration::from_millis(300)).await;
"done\n"
}))
.layer(SlowRequestLayer { threshold: Duration::from_millis(100) });
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
-
poll_readymust forward to the inner service so back-pressure propagates. -
callmust not hold&mut selfacross the future: the inner future is created synchronously and moved into the boxedasyncblock. -
A subtle tower rule:
poll_readyreserves capacity on this clone of the service. Ifcallclonesinnerand calls the clone, usestd::mem::replaceto swap the ready service in, as the axum documentation explains; the version above callsself.innerdirectly, which is always correct. -
For zero-allocation middleware, write a custom
Futuretype withpin-project-liteinstead of boxing; tower-http’s layers are built that way.
Where the books are outdated
Yoon (Beginning Axum, chapter 4) writes middleware with from_fn using the Next type without generics, which is
the 0.7+ form (0.6 had Next<B> with a body type parameter, and Request<B> instead of axum::extract::Request).
Code that still names Next<Body> or Request<Body> in middleware signatures must drop the parameter.