问题
- Axum 中间件的执行顺序 - 请求与响应
结论
-
Axum 的中间件系统来自于Tower,可以想象成一个洋葱
- 请求进来时 :像剥洋葱,从最外层一层一层往里面走,最后到达最最核心的Handler。
- 响应返回时 :像包洋葱,从最核心的Handler一层一层往外包,最后离开最外层。 关键在于注册顺序 :你最后 用
.layer()注册的中间件会成为最外层的洋葱皮 所以: - 请求路径是: 最后注册的中间件 -> ... -> 最先注册的中间件 -> Handler
- 响应路径是: Handler -> 最先注册的中间件 -> ... -> 最后注册的中间件 如果写了:
rust.layer(A) // 最先注册 .layer(B) .layer(C) // 最后注册那么实际执行顺序是: - 请求:C -> B -> A -> Handler - 响应:Handler -> A -> B -> C 这就是为什么会感觉顺序是反着的,文档里面也说了: "Generally speaking, this results in middleware being executed from bottom to top"
原理
Tower 的包装逻辑:
rust
Router::new()
.route("/", get(handler))
.layer(A) // 包住 handler
.layer(B) // 包住 A(handler)
.layer(C) // 包住 B(A(handler))
实际上构建出来的结构是:C(B(A(handler)))。 在 Rust 代码中,.layer() 方法接收一个 Layer,返回一个包装了内部 Service 的新的 Service。 官方文档中提到的 bottom to top,是因为在代码书写上,先写的在最下面(bottom),后写的在最上面(top),但执行请求时是从最上面(最后注册的)开始剥洋葱。
rust
// 伪代码
let service = handler;
let service_a = A::new(service); // A 包住 handler
let service_b = B::new(service_a); // B 包住 A
let service_c = C::new(service_b); // C 包住 B
// 请求进入 service_c.call(),它内部会调用 service_b.call()
// 所以请求顺序是 C -> B -> A
// 响应返回时,在各自的 call 方法里从内向外处理,顺序是 A -> B -> C
代码
rust
use axum::{
middleware::{self, Next},
routing::get,
Router,
http::Request,
response::Response,
};
async fn mw_a(req: Request<axum::body::Body>, next: Next) -> Response {
println!("A: 请求进入");
let res = next.run(req).await;
println!("A: 响应返回");
res
}
async fn mw_b(req: Request<axum::body::Body>, next: Next) -> Response {
println!("B: 请求进入");
let res = next.run(req).await;
println!("B: 响应返回");
res
}
async fn handler() -> &'static str {
println!("Handler 执行");
"ok"
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(handler))
.layer(middleware::from_fn(mw_a)) // 先注册
.layer(middleware::from_fn(mw_b)); // 后注册,最外层
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("访问 http://127.0.0.1:3000 查看控制台输出");
axum::serve(listener, app).await.unwrap();
}
运行后访问 http://127.0.0.1:3000,控制台输出:
text
B: 请求进入
A: 请求进入
Handler 执行
A: 响应返回
B: 响应返回
踩坑
踩坑 1:误以为先注册的先执行。 排查:打印日志验证。发现请求先进入第二个注册的中间件。 踩坑 2:随便用 .layer() 加鉴权,结果请求一个不存在的接口,返回的是 401 而不是 404。 原因:.layer() 作用于整个 Router,包括 fallback(404)。请求未匹配到路由时,依然会穿过 .layer() 注册的中间件,鉴权失败自然返回 401。 解决:改用 .route_layer(),它仅在匹配到具体路由时才执行。这样就保证了不存在的接口直接返回 404。 踩坑 3:中间件里返回 Err,程序直接 panic 或者返回 500 而不是自定义的错误信息。 原因:普通的中间件错误不会自动转换为 HTTP 响应。 解决:引入 HandleErrorLayer,放在可能报错的中间件的外层(后注册)去捕获错误并转为合适的 Response。 踩坑 4:给已经在 .layer() 中注册的 Router 动态添加了新路由,新路由没有生效该中间件。 原因:.layer() 和 .route_layer() 都只作用于注册那一刻已经存在的路由。后加的路由不会自动套上之前的 layer。 解决:在最后一步统一加 layer,或者确保先定义完所有路由,再加中间件。
面试怎么答
Axum 中间件基于 Tower 的 Service 组合模型,.layer() 是包裹语义。就像洋葱一样,后注册的 layer 会包裹住之前已经组合好的 Service,因此它位于最外层。 请求从最外层进入,逐层向内到达 Handler;响应则由 Handler 逐层向外返回。 面试中常考的一个区别是 .layer() 和 .route_layer():前者对 404 等未匹配路由也会生效,后者只对已匹配的具体路由生效,所以鉴权中间件通常推荐用 .route_layer()。 如果中间件可能抛出错误,需要在它的外层加 HandleErrorLayer 来处理错误响应。
参考
- Axum 官方文档 - Middleware:docs.rs/axum/latest... (重点看 from_fn 和 layer 的说明)
- Axum 官方文档 - Router::layer:docs.rs/axum/latest...
- Axum 官方文档 - Router::route_layer:docs.rs/axum/latest...
- Tower 官方文档 - Service trait:docs.rs/tower/lates...
- Axum 官方 examples 仓库中的 middleware 示例:github.com/tokio-rs/ax...