axum 中间件完全指南:基于 tower 的中间件架构、执行顺序与错误处理实战
【免费下载链接】axumHTTP routing and request-handling library for Rust that focuses on ergonomics and modularity项目地址: https://gitcode.com/GitHub_Trending/ax/axum
导读
本文以 axum/src/docs/middleware.md 为核心文档,系统讲解 axum 中间件体系的完整设计:从"为什么 axum 没有自研中间件系统"的设计哲学,到Router::layer/route_layer/Handler::layer的挂载方式,再到中间件洋葱模型与tower::ServiceBuilder组合顺序,最后深入from_fn、from_extractor、手写tower::Service四种编写路径及错误处理、状态访问、URI 重写等高频实战问题。读完本文,你将掌握在 axum 应用中正确选择、编排、编写中间件,并理解其底层执行语义。
一、设计哲学:axum 没有自己的中间件系统
axum 的独特之处在于:它没有自研一套专属中间件体系,而是直接与 [tower] 深度集成。这意味着tower生态与tower-http生态中的全部中间件(日志、CORS、压缩、超时、限流等)都可以开箱即用地接入 axum,无需任何适配层。
从源码结构看,这一设计贯穿整个仓库:
- axum/src/middleware/mod.rs 中的模块文档直接
include_str!("../docs/middleware.md"),即本文核心文档就是该模块的官方指南; - 模块对外导出的
from_fn、from_fn_with_state、from_extractor、map_request、map_response等工具,全部实现为tower::Layer/tower::Service组合,而非自造抽象。
虽然使用 axum 中间件不要求完全理解 tower,但官方建议至少掌握 tower 的基础概念(可参考 tower 官方的 guides 系列),并通读 [tower::ServiceBuilder] 的文档——因为ServiceBuilder是组合多个中间件的事实标准工具。
二、在哪里挂载中间件:四种入口
axum 允许你在几乎任何位置添加中间件:
| 挂载位置 | API | 作用范围 |
|---|---|---|
| 整个路由树 | Router::layer、Router::route_layer | 包裹整个Router或其中所有已注册路由 |
| 方法路由器 | MethodRouter::layer、MethodRouter::route_layer | 包裹单个MethodRouter(如某个get(...)链) |
| 单个处理器 | Handler::layer | 只包裹单个 handler,返回Layered包装 |
2.1Router::layer:包裹整个路由树
从 Router::layer 的实现 可以看到,它对内部path_router和catch_all_fallback分别应用layer,从而覆盖路由器中的全部既有路由。
使用它时有两个关键注意点(参见 axum/src/docs/routing/layer.md):
- 只作用于"已存在"的路由:必须先添加路由(及 fallback),再调用
layer;之后再新增的路由不会带上该中间件。 - 运行时机在路由匹配之后:
Router::layer的中间件无法重写请求 URI(详见本文第八节)。
2.2Router::route_layer:仅对匹配到的路由生效
route_layer与layer的差别在于:中间件只有在请求匹配到某个路由时才会运行。这特别适合授权类中间件——否则一个本应返回404 Not Found的请求,可能因为全局中间件提前return Err(...)而变成401 Unauthorized,掩盖了"路由不存在"这一事实。route_layer的文档示例(axum/src/docs/routing/route_layer.md)验证了这一行为:
GET /foo+ 有效 token →200 OKGET /foo+ 无效 token →401 UnauthorizedGET /not-found+ 无效 token →404 Not Found(中间件不运行)
另外注意:route_layer在路由器还没有声明任何路由时会panic,因为此时该 layer 不会产生任何效果,这通常是个 bug;泛型代码中可先用Router::has_routes判断。
2.3 仅对部分路由应用中间件:用merge组合
如果只想让部分路由带上中间件,官方推荐先用各自的路由器挂载中间件,再通过Router::merge合并:
use axum::{routing::get, Router}; use tower_http::{trace::TraceLayer, compression::CompressionLayer}; let with_tracing = Router::new() .route("/foo", get(|| async {})) .layer(TraceLayer::new_for_http()); let with_compression = Router::new() .route("/bar", get(|| async {})) .layer(CompressionLayer::new()); // Merge everything into one `Router` let app = Router::new() .merge(with_tracing) .merge(with_compression);三、一次挂载多个中间件:优先使用ServiceBuilder
当需要同时应用多个中间件时,官方强烈建议使用tower::ServiceBuilder一次性组合,而不是反复调用layer/route_layer:
use axum::{ routing::get, Extension, Router, }; use tower_http::{trace::TraceLayer}; use tower::ServiceBuilder; async fn handler() {} #[derive(Clone)] struct State {} let app = Router::new() .route("/", get(handler)) .layer( ServiceBuilder::new() .layer(TraceLayer::new_for_http()) .layer(Extension(State {})) ); # let _: Router = app;这样做的原因与执行顺序密切相关(见下一节):ServiceBuilder会把所有 layer 组合成一个,并按"从上到下"的顺序执行,更符合人类直觉。
四、执行顺序:洋葱模型与两种组合方向
4.1 多次调用layer:从下往上执行
当你用Router::layer(或同类方法)依次添加中间件时,所有先前添加的路由都会被新中间件包裹,从执行效果上看,中间件自底向上运行:
use axum::{routing::get, Router}; async fn handler() {} let app = Router::new() .route("/", get(handler)) .layer(layer_one) .layer(layer_two) .layer(layer_three);可以把它想象成一个洋葱——每一层新中间件都包裹住前面所有层:
requests | v +----- layer_three -----+ | +---- layer_two ----+ | | | +-- layer_one --+ | | | | | | | | | | | handler | | | | | | | | | | | +-- layer_one --+ | | | +---- layer_two ----+ | +----- layer_three -----+ | v responses即请求的完整链路是:layer_three→layer_two→layer_one→handler,然后响应原路返回:layer_one→layer_two→layer_three。
需要注意,这只是便于理解的思维模型。实际上任何中间件都可以提前短路返回(例如请求未通过授权时直接返回响应而不调用下一层),此时响应不会经过更内层的中间件。
4.2ServiceBuilder:从上往下执行
同样是三个 layer,如果改用ServiceBuilder组合:
use tower::ServiceBuilder; use axum::{routing::get, Router}; let app = Router::new() .route("/", get(handler)) .layer( ServiceBuilder::new() .layer(layer_one) .layer(layer_two) .layer(layer_three), );ServiceBuilder会把所有 layer 合成一个,执行顺序变为从上到下:请求先到达layer_one,再layer_two、layer_three,最后进入handler;响应再按layer_three→layer_two→layer_one的顺序冒泡返回。
"从上到下"的执行顺序在心理上更容易跟踪和推理,这也是官方推荐ServiceBuilder的重要原因之一。
五、常用中间件速查(tower-http 生态)
既然 axum 直接复用 tower 生态,以下中间件即可直接使用:
| 中间件 | 模块 | 用途 |
|---|---|---|
TraceLayer | tower_http::trace | 高层级的 tracing / 日志 |
CorsLayer | tower_http::cors | 处理跨域 CORS |
CompressionLayer | tower_http::compression | 对响应自动压缩 |
RequestIdLayer/PropagateRequestIdLayer | tower_http::request_id | 设置与传播请求 ID |
TimeoutLayer | tower_http::timeout::TimeoutLayer | 请求超时控制 |
六、编写自己的中间件:四条路径与取舍
axum 提供了多种编写中间件的方式,抽象层级不同,各有优劣。
6.1axum::middleware::from_fn:async/await 风格,最易上手
使用axum::middleware::from_fn编写中间件,适合:
- 不想手写 Future,习惯使用熟悉的
async/await语法; - 不打算把中间件发布成 crate 供他人使用(此类中间件只兼容 axum)。
从 from_fn 的实现 可以确认其函数签名约束:
- 必须是一个
async fn; - 可以接收零个或多个实现
FromRequestParts的提取器(如HeaderMap); - 倒数第二个参数必须是恰好一个实现
FromRequest的提取器(Request满足); - 最后一个参数必须是
Next; - 返回值需实现
IntoResponse。
典型的认证中间件写法:
use axum::{ Router, http::{StatusCode, HeaderMap}, middleware::{self, Next}, response::Response, extract::Request, routing::get, }; async fn auth( headers: HeaderMap, // FromRequestParts 提取器 request: Request, // 最后一个提取器,实现 FromRequest next: Next, // 最后一个参数 ) -> Result<Response, StatusCode> { match get_token(&headers) { Some(token) if token_is_valid(token) => { let response = next.run(request).await; Ok(response) } _ => Err(StatusCode::UNAUTHORIZED), } } let app = Router::new() .route("/", get(|| async { /* ... */ })) .route_layer(middleware::from_fn(auth));源码实现细节值得留意:中间件内可自由使用提取器(比如HeaderMap、Request),提取失败时会把 rejection 直接into_response();Next内部持有BoxCloneSyncService,其run方法的错误类型是Infallible(见 from_fn.rs 中 Next 的定义),因此调用方无需处理错误分支。
6.2axum::middleware::from_extractor:提取器即中间件
使用axum::middleware::from_extractor,适合那种"有时当提取器、有时当中间件"的类型。如果某个类型只打算当中间件用,则优先选择from_fn。
其语义(见 from_extractor.rs 的文档):提取器成功则丢弃值、继续调用内部服务;提取失败则直接返回 rejection,内部服务不会被调用。典型场景是写一个RequireAuth提取器做授权校验,然后通过route_layer一次性保护多个路由:
use axum::{ extract::FromRequestParts, middleware::from_extractor, routing::{get, post}, Router, http::{header, StatusCode, request::Parts}, }; struct RequireAuth; impl<S> FromRequestParts<S> for RequireAuth where S: Send + Sync, { type Rejection = StatusCode; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { let auth_header = parts .headers .get(header::AUTHORIZATION) .and_then(|value| value.to_str().ok()); match auth_header { Some(auth_header) if token_is_valid(auth_header) => Ok(Self), _ => Err(StatusCode::UNAUTHORIZED), } } } let app = Router::new() .route("/", get(handler)) .route("/foo", post(other_handler)) // 提取器会在所有路由之前运行 .route_layer(from_extractor::<RequireAuth>());⚠️ 注意:如果提取器会消费请求体(如String、Bytes),原地会留下空 body,后续提取器或 handler 将无法再读取请求体。
6.3 tower 组合子:轻量请求/响应修改
tower 提供若干工具组合子,适合做"加个头"这类小而临时的操作:
ServiceBuilder::map_requestServiceBuilder::map_responseServiceBuilder::thenServiceBuilder::and_then
适用场景:想执行一个小的即席操作(如添加响应头),且不打算发布为通用 crate。
6.4 手写tower::Service+Pin<Box<dyn Future>>:最大控制力
需要最大控制力(以及更底层的 API)时,可以直接实现tower::Service。官方给出了完整模板(原样继承):
use axum::{ response::Response, body::Body, extract::Request, }; use futures_core::future::BoxFuture; use tower::{Service, Layer}; use std::task::{Context, Poll}; #[derive(Clone)] struct MyLayer; impl<S> Layer<S> for MyLayer { type Service = MyMiddleware<S>; fn layer(&self, inner: S) -> Self::Service { MyMiddleware { inner } } } #[derive(Clone)] struct MyMiddleware<S> { inner: S, } impl<S> Service<Request> for MyMiddleware<S> where S: Service<Request, Response = Response> + Send + 'static, S::Future: Send + 'static, { type Response = S::Response; type Error = S::Error; // `BoxFuture` 是 `Pin<Box<dyn Future + Send + 'a>>` 的类型别名 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) } fn call(&mut self, request: Request) -> Self::Future { let future = self.inner.call(request); Box::pin(async move { let response: Response = future.await?; Ok(response) }) } }使用这种方式的时机:
- 中间件需要可配置(例如通过
tower::Layer上的 builder 方法,如tower_http::trace::TraceLayer的配置项); - 打算把中间件发布成 crate 供他人使用;
- 不习惯(或不想)手写自己的 Future。
关键设计原则:错误类型被定义为S::Error,意味着你的中间件通常不产生错误。原则是:尽量总是返回一个响应,不要用自定义错误类型"中途退出"。例如第三方库返回了专用错误类型,应将其转换为合理的响应并返回Ok(该响应)。
如果你确实实现了自定义错误类型(如type Error = BoxError,或任何非Infallible的类型),则必须配合HandleErrorLayer将错误转换为响应:
ServiceBuilder::new() .layer(HandleErrorLayer::new(|_: BoxError| async { // 因为 axum 使用 infallible 错误,你必须在这里处理中间件返回的自定义错误 StatusCode::BAD_REQUEST })) .layer( // <你的真正会返回错误的 layer> );6.5 手写tower::Service+ 自定义 Future:极致性能
如果熟悉(或想学习)手写 Future,并且需要尽可能多的控制力,可以使用不带 boxed future 的tower::Service:
- 追求最低开销;
- 中间件需要可配置;
- 打算发布为 crate(甚至作为 tower-http 的一部分);
- 熟悉 async Rust 底层机制。
tower 官方的"Building a middleware from scratch"指南是学习这一路径的最佳起点。
七、中间件的错误处理:HandleErrorLayer
axum 的错误处理模型要求每个 handler 必须始终返回响应。但中间件是应用引入错误的可能途径之一:如果错误一路传到 hyper,连接会被直接关闭而不发送任何响应。因此 axum 要求中间件产生的错误必须被优雅处理。
典型做法是用HandleErrorLayer把错误转成响应。注意HandleErrorLayer必须放在会产生错误的中间件之上(因为只有它才能接收到下层返回的错误):
use axum::{ routing::get, error_handling::HandleErrorLayer, http::StatusCode, BoxError, Router, }; use tower::{ServiceBuilder, timeout::TimeoutLayer}; use std::time::Duration; async fn handler() {} let app = Router::new() .route("/", get(handler)) .layer( ServiceBuilder::new() // 这个中间件放在 `TimeoutLayer` 之上,因为它要接收 // `TimeoutLayer` 返回的错误 .layer(HandleErrorLayer::new(|_: BoxError| async { StatusCode::REQUEST_TIMEOUT })) .layer(TimeoutLayer::new(Duration::from_secs(10))) );axum 错误处理模型的完整细节参见 axum/src/docs/error_handling.md:axum 通过类型系统强制所有服务错误类型为Infallible;即便 handler 返回Result<String, StatusCode>,Err也会被StatusCode的IntoResponse实现转成响应发回客户端,而不被视为"错误"。HandleErrorLayer还支持运行提取器(如Method、Uri),最后一个参数是错误本身,便于构造更丰富的错误响应。
八、路由到服务/中间件与背压(backpressure)
将请求路由到多个服务之一与背压天生不兼容:理想情况下你应该先确认服务就绪再调用它,但要确定调用哪个服务,你首先得拿到请求……这构成了矛盾。
业界有两种解法:
- 等所有目标服务都就绪,路由器才就绪——这是
tower::steer::Steer采用的方案; - 始终认为所有服务就绪(
Service::poll_ready恒返回Poll::Ready(Ok(()))),把真正的就绪检查推迟到Service::call返回的响应 future 内部去驱动——适用于"不在乎背压、总是就绪"的服务。
axum 假定应用中的所有服务都不关心背压,因此采用第二种策略。由此带来的约束:
- 应避免路由到(或使用)关心背压的服务/中间件;至少要配合
tower::load_shed快速丢弃请求,避免积压。 - 如果
poll_ready返回错误,该错误会在call的响应 future 中返回,而不是在poll_ready中返回;此时底层服务不会被丢弃,仍会用于后续请求。期望"poll_ready 失败即被丢弃"的服务不应与 axum 搭配使用。
可行的折衷:把背压敏感的中间件包在整个应用外面。由于 axum 应用本身就是tower::Service,可以直接用ServiceBuilder包裹:
use axum::{ routing::get, Router, }; use tower::ServiceBuilder; async fn handler() { /* ... */ } let app = Router::new().route("/", get(handler)); let app = ServiceBuilder::new() .layer(some_backpressure_sensitive_middleware) .service(app);但这样包裹整个应用时,要确保错误仍然被妥善处理。另外,由 async 函数创建的 handler 不关心背压、始终就绪——如果你没用任何 tower 中间件,则完全无需担心上述问题。
九、在中间件中访问状态(State)
如何让中间件拿到状态,取决于中间件的编写方式。
9.1from_fn类中间件
直接使用axum::middleware::from_fn_with_state即可让from_fn中间件提取State。注意普通from_fn不支持提取State,这是两者最直观的区别。
9.2 自定义tower::Layer中访问状态
在自定义 Layer/Service 中,把状态克隆进服务结构体即可(完整模板):
use axum::{ Router, routing::get, middleware::{self, Next}, response::Response, extract::{State, Request}, }; use tower::{Layer, Service}; use std::task::{Context, Poll}; #[derive(Clone)] struct AppState {} #[derive(Clone)] struct MyLayer { state: AppState, } impl<S> Layer<S> for MyLayer { type Service = MyService<S>; fn layer(&self, inner: S) -> Self::Service { MyService { inner, state: self.state.clone(), } } } #[derive(Clone)] struct MyService<S> { inner: S, state: AppState, } impl<S, B> Service<Request<B>> for MyService<S> where S: Service<Request<B>>, { type Response = S::Response; type Error = S::Error; type Future = S::Future; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { self.inner.poll_ready(cx) } fn call(&mut self, req: Request<B>) -> Self::Future { // 在这里使用 `self.state` // 可参考 `axum::RequestExt` 了解如何直接从 `Request` 运行提取器 self.inner.call(req) } } async fn handler(_: State<AppState>) {} let state = AppState {}; let app = Router::new() .route("/", get(handler)) .layer(MyLayer { state: state.clone() }) .with_state(state);挂载时通过.with_state(state)完成最终状态注入(with_state的实现见 axum/src/routing/mod.rs,它把状态同时分发到path_router与 fallback 路由)。
十、从中间件向 handler 传递数据:请求扩展(Extensions)
中间件与 handler 之间可以通过**请求扩展(request extensions)**传递数据。经典案例:认证中间件把CurrentUser塞进请求扩展,handler 再用Extension提取器取回:
use axum::{ Router, http::StatusCode, routing::get, response::{IntoResponse, Response}, middleware::{self, Next}, extract::{Request, Extension}, }; #[derive(Clone)] struct CurrentUser { /* ... */ } async fn auth(mut req: Request, next: Next) -> Result<Response, StatusCode> { let auth_header = req.headers() .get(http::header::AUTHORIZATION) .and_then(|header| header.to_str().ok()); let auth_header = if let Some(auth_header) = auth_header { auth_header } else { return Err(StatusCode::UNAUTHORIZED); }; if let Some(current_user) = authorize_current_user(auth_header).await { // 把当前用户插入请求扩展,供 handler 提取 req.extensions_mut().insert(current_user); Ok(next.run(req).await) } else { Err(StatusCode::UNAUTHORIZED) } } async fn authorize_current_user(auth_token: &str) -> Option<CurrentUser> { // ... unimplemented!() } async fn handler( // 提取中间件设置的用户信息 Extension(current_user): Extension<CurrentUser>, ) { // ... } let app = Router::new() .route("/", get(handler)) .route_layer(middleware::from_fn(auth));注意:响应扩展(response extensions)也可以使用,但请求扩展不会自动迁移到响应扩展;如果需要,你必须手动为所需扩展完成迁移。
十一、在中间件中重写请求 URI:绕开"路由已匹配"的限制
通过Router::layer添加的中间件在路由匹配之后才运行,因此它无法用于"重写请求 URI"这类必须在路由决策之前发生的操作。
解决方法:把中间件包在整个Router外面(Router实现了tower::Service,所以可以这样做):
use tower::Layer; use axum::{ Router, ServiceExt, // 提供 `into_make_service` response::Response, middleware::Next, extract::Request, }; fn rewrite_request_uri<B>(req: Request<B>) -> Request<B> { // ... req } // 可以是任意 `tower::Layer` let middleware = tower::util::MapRequestLayer::new(rewrite_request_uri); let app = Router::new(); // 把 layer 包在整个 `Router` 外, // 这样中间件会在 `Router` 收到请求之前运行 let app_with_middleware = middleware.layer(app); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app_with_middleware.into_make_service()).await;这样处理之后,URI 重写发生在路由匹配之前,Router才能基于改写后的 URI 完成正确的路由分发。
十二、更多参考
- 中间件模块全部导出见 axum/src/middleware/mod.rs,包括
from_fn、from_fn_with_state、from_extractor、map_request、map_response及其 Layer/Service/响应 Future 类型; Router::layer与Router::route_layer的详细文档见 axum/src/docs/routing/layer.md 与 axum/src/docs/routing/route_layer.md;- axum 错误处理模型的整体说明见 axum/src/docs/error_handling.md;
- 中间件与错误处理的完整可运行示例可参考仓库
examples/下的error-handling、consume-body-in-extractor-or-middleware、request-id等目录。
【免费下载链接】axumHTTP routing and request-handling library for Rust that focuses on ergonomics and modularity项目地址: https://gitcode.com/GitHub_Trending/ax/axum
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考