Axum 中间件执行顺序

问题

  1. Axum 中间件的执行顺序 - 请求与响应

结论

  1. 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 来处理错误响应。

参考

相关推荐
徐小黑ACG3 小时前
Golang 基础06 接口interface
开发语言·后端·golang
三小河3 小时前
从 Markdown 到 Generative UI:AI 如何从“生成答案”进化到“生成界面”?
前端·人工智能·后端
韩振方3 小时前
为什么加了 sudo,写配置文件还是 Permission denied?
后端
牛奔4 小时前
mise 管理多版本 Go 项目
开发语言·前端·chrome·后端·golang
打工仔折腾 AI5 小时前
FaceFusion本地换脸实战:Windows整合包、模型选择与遮罩调参记录
人工智能·windows·后端·python·深度学习·性能优化·ai agent 实战
Nebula_g5 小时前
JavaSE拓展:工具类Executors
java·开发语言·后端·spring·基础·javase
弈栈录5 小时前
Java 后端高并发设计:线程池、限流、熔断与降级
后端·架构
邱杰4705 小时前
从零手写数智人事系统(十二):部门树、角色授权与菜单维护
后端
PC2005_cloud5 小时前
AI 汉字漂移实录:它为什么总把「学习」写成日语「学習」
前端·后端