axum 路由层中间件指南:用 Router::layer 为整组路由统一添加 Tower 中间件
【免费下载链接】axumHTTP routing and request-handling library for Rust that focuses on ergonomics and modularity项目地址: https://gitcode.com/GitHub_Trending/ax/axum
本篇技术指南围绕 axum 的Router::layer方法展开,讲解如何基于 Tower 生态为 Router 中已注册的全部路由统一附加中间件(如日志追踪、超时、CORS、压缩等),并深入剖析其"只作用于已有路由、运行在路由匹配之后"的语义,以及它与route_layer、MethodRouter::layer、Handler::layer之间的适用边界。读完本文,你将能正确、可预期地为整组路由编排中间件,理解执行顺序、URI 改写限制与错误处理影响,并能在真实项目中落地可运行的配置。
Router::layer 是什么
axum 没有自成一体的中间件体系,而是直接集成 [tower] 生态——这意味着 tower 与 tower-http 中所有现成中间件都能直接用于 axum(见 axum/src/middleware/mod.rs 中引入的官方文档 middleware.md 开头说明)。Router::layer正是把这种能力应用到"一组路由"上的入口:它接收一个 [tower::Layer],并将该 Layer 包装到 Router 中当前已存在的所有路由上,为请求增加额外的处理环节。
从源码看,Router::layer的实现非常直观(axum/src/routing/mod.rs):
pub fn layer<L>(self, layer: L) -> Self where L: Layer<Route> + Clone + Send + Sync + 'static, L::Service: Service<Request> + Clone + Send + Sync + 'static, <L::Service as Service<Request>>::Response: IntoResponse + 'static, <L::Service as Service<Request>>::Error: Into<Infallible> + 'static, <L::Service as Service<Request>>::Future: Send + 'static, { map_inner!(self, this => RouterInner { path_router: this.path_router.layer(layer.clone()), default_fallback: this.default_fallback, catch_all_fallback: this.catch_all_fallback.map(|route| route.layer(layer)), }) }可见它同时作用于两处:
path_router:所有已注册路由对应的端点(Endpoint),由 PathRouter::layer 逐一将每个端点的Route用该 Layer 包装;catch_all_fallback:Router 的兜底(fallback)路由同样会被 Layer 包装。
这带来一个容易被忽略的事实:通过Router::layer添加的中间件同样会作用于请求未命中任何路由时的 fallback 处理。仓库测试 middleware_still_run_for_unmatched_requests 验证了这一点——对一个匹配不到的路径发请求,计数器中间件依然被调用。
方法签名与依赖约束
layer的泛型约束体现了 Tower 与 axum 的对接要求:
L: Layer<Route> + Clone + Send + Sync + 'static:Layer 本身必须可克隆、可跨线程发送与同步;L::Service: Service<Request> + Clone + Send + Sync + 'static:包装后的服务需以 axum 的Request为输入;Response: IntoResponse:服务响应必须能转换为 HTTP 响应;Error: Into<Infallible>:服务错误必须能转换为Infallible——这正是 axum "处理器永远返回响应、错误必须在中间件链内消化" 的错误处理模型的体现。
关键语义一:只作用于已存在的路由
文档明确强调:中间件只应用于当前已注册的路由。因此正确的调用顺序是"先添加路由(以及 fallback),再调用layer";在调用layer之后新增的路由,不会得到该中间件。
use axum::{routing::get, Router}; use tower_http::trace::TraceLayer; let app = Router::new() .route("/foo", get(|| async {})) .route("/bar", get(|| async {})) .layer(TraceLayer::new_for_http());上面示例中,/foo与/bar两个路由都会被TraceLayer包装。如果调换顺序:
let app = Router::new() .layer(TraceLayer::new_for_http()) // 此时 Router 中还没有任何路由 .route("/foo", get(|| async {})); // /foo 不会经过 TraceLayer仓库测试 middleware_applies_to_routes_above 正是这一语义的直接证据:先注册/one再挂载TimeoutLayer,随后注册的/two不受超时中间件影响——/one返回REQUEST_TIMEOUT,而/two正常返回OK。
从实现上理解,这是因为PathRouter::layer是对当前routes向量做一次性map包装(axum/src/routing/path_router.rs),后续route新增的端点直接 push 进同一个向量,天然不会经过之前的包装。
只给部分路由加中间件:与 Router::merge 组合
如果只想让中间件作用于部分路由,文档给出的方案是利用Router::merge把"各自已挂好中间件"的 Router 合并成一个:
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()); // 合并为一个 Router:/foo 走 TraceLayer,/bar 走 CompressionLayer let app = Router::new() .merge(with_tracing) .merge(with_compression);这里merge把两个子 Router 的路径与 fallback 合入同一个 Router(见 Router::merge 及其配套文档 merge.md)。注意两点:
- 合并时两个 Router 的状态类型必须一致;若不同,可先用
Router::with_state提供状态统一类型; - 两个 Router 中最多只能有一个显式 fallback,否则会在合并时 panic(merge.md 中的 Panics 说明)。
多个中间件:推荐使用 tower::ServiceBuilder
当需要叠加多个中间件时,文档建议使用 [tower::ServiceBuilder] 一次性组合,而不是重复调用layer:
use axum::{ routing::get, Extension, Router, }; use tower::ServiceBuilder; use tower_http::trace::TraceLayer; 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 {})), );ServiceBuilder会把多个 Layer 组合成一个整体,使中间件自上而下执行(先挂的TraceLayer先收到请求),这比多次调用layer产生的"从下往上"执行更符合直觉、更易维护(详见 middleware.md 中 Ordering 一节,其中给出了完整的"洋葱模型" ASCII 图:layer_three → layer_two → layer_one → handler,响应再反向回传)。
执行顺序的两种心智模型
- 多次调用
Router::layer:后添加的 Layer 包裹先添加的,如同洋葱外层包内层。请求先进入最后调用的layer_three,最后进入最早调用的layer_one,响应按相反顺序回传; ServiceBuilder组合:所有 Layer 被组合为一个整体,按书写顺序自上而下执行——layer_one最先收到请求,layer_three最接近 handler。
如果某个中间件(例如授权校验)可能提前短路返回,这一顺序差异会直接影响请求是否到达后续中间件与 handler,是排查"为什么我的中间件没生效"的高频原因。
关键语义二:运行在路由匹配之后,不能改写请求 URI
文档特别强调:Router::layer添加的中间件在路由匹配之后运行,因此不能用来改写请求 URI(改写发生在路由匹配之前才有意义)。
对于确实需要改写 URI 的场景,middleware.md 提供了可行方案:因为Router本身实现了Service,可以把中间件包在整个 Router 外层,这样它就能在路由匹配之前运行:
use tower::Layer; use axum::{ Router, ServiceExt, // 提供 into_make_service middleware::Next, extract::Request, }; fn rewrite_request_uri<B>(req: Request<B>) -> Request<B> { // 在这里改写 req.uri() ... req } // 可以是任意 tower::Layer let middleware = tower::util::MapRequestLayer::new(rewrite_request_uri); let app = Router::new(); // 把中间件包在整个 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;错误处理的影响
由于 axum 要求 handler 永远返回响应(错误类型为Infallible),引入可能产生错误的中间件时必须用 [HandleErrorLayer] 消化错误,否则 hyper 收到错误会直接关闭连接而不返回任何响应。典型组合是HandleErrorLayer在上、会出错的 Layer 在下:
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 之上,接收其抛出的错误 .layer(HandleErrorLayer::new(|_: BoxError| async { StatusCode::REQUEST_TIMEOUT })) .layer(TimeoutLayer::new(Duration::from_secs(10))), );关于 axum 错误处理模型的完整说明见 error_handling 与 axum/src/error_handling/mod.rs。
Router::layer 与 route_layer 的取舍
与Router::layer语义最接近的姊妹方法是Router::route_layer(源码见 axum/src/routing/mod.rs):它同样作用于已有路由,但中间件只有在请求匹配到某条路由时才会运行。这一点对"可能提前返回"的中间件(如鉴权)至关重要:
use axum::{routing::get, Router}; use tower_http::validate_request::ValidateRequestHeaderLayer; let app = Router::new() .route("/foo", get(|| async {})) .route_layer(ValidateRequestHeaderLayer::bearer("password")); // 带合法 token 请求 GET /foo → 200 OK // token 非法请求 GET /foo → 401 Unauthorized // token 非法请求 GET /not-found → 404 Not Found(不会被中间件拦截)如果改用Router::layer挂同样的鉴权中间件,由于它连 fallback 一起包装,未匹配路径也会被鉴权拦截,可能把404 Not Found变成401 Unauthorized。仓库测试 route_layer 完整验证了上述三条行为。
此外,route_layer在 Router 上还没有任何路由时调用会直接 panic(PathRouter::route_layer 中routes.is_empty()检查),因为它此时是无效的空操作;泛型代码中可先用Router::has_routes判断。
更细粒度的中间件挂载点
Router::layer面向"一组路由",axum 还提供另外两个粒度的挂载点:
| 挂载点 | 作用范围 | 典型场景 |
|---|---|---|
Router::layer | Router 中全部已有路由 + fallback | 全局日志、超时、CORS |
Router::route_layer | 仅匹配到路由的请求 | 鉴权等会提前返回的中间件 |
MethodRouter::layer | 单一路径下全部 HTTP 方法 | 对某条路径单独限流 |
Handler::layer | 单个 handler | 只针对特定处理方法附加中间件 |
MethodRouter::layer的官方文档示例(method_routing/layer.md)展示了在单条路径上挂并发限制中间件:
use axum::{routing::get, Router}; use tower::limit::ConcurrencyLimitLayer; async fn handler() {} let app = Router::new().route( "/", // 所有发送到 GET / 的请求都会经过 ConcurrencyLimitLayer get(handler).layer(ConcurrencyLimitLayer::new(64)), );而Handler::layer的用法则是在单个 handler 上直接链式调用(仓库测试 middleware_on_single_route 展示了get(handle.layer(TraceLayer::new_for_http()))的写法)。
编写自己的中间件
Router::layer接受任何tower::Layer,因此写中间件有多个抽象层级可选(详见 middleware.md):
axum::middleware::from_fn:用熟悉的async/await编写,适合不打算发布成独立 crate 的中间件;axum::middleware::from_extractor:当你希望某个类型既能作为提取器又能作为中间件时使用;tower::ServiceBuilder的map_request/map_response/then/and_then组合子:适合加个响应头这类临时小改动;- 手写
tower::Service+Pin<Box<dyn Future>>:适合可配置、打算发布的中间件(如TraceLayer的形态); - 手写
tower::Service+ 自定义 Future:追求最低开销时使用。
无论哪种方式,Router::layer都能把它们应用到整组路由上。
实战:一个完整的日志追踪示例
结合仓库自带示例 examples/tracing-aka-logging/src/main.rs,一个"用Router::layer给整组路由加 TraceLayer"的完整可运行程序如下(示例可用cargo run -p example-tracing-aka-logging运行):
use axum::{ body::Bytes, extract::MatchedPath, http::{HeaderMap, Request}, response::{Html, Response}, routing::get, Router, }; use std::time::Duration; use tokio::net::TcpListener; use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer}; use tracing::{info_span, Span}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[tokio::main] async fn main() { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { format!("{}=debug,tower_http=debug,axum::rejection=trace", env!("CARGO_CRATE_NAME")) .into() }), ) .with(tracing_subscriber::fmt::layer()) .init(); let app = Router::new() .route("/", get(handler)) // 关键:把 TraceLayer 挂到整个 Router 上,所有已注册路由都会经过它 .layer( TraceLayer::new_for_http() .make_span_with(|request: &Request<_>| { let matched_path = request .extensions() .get::<MatchedPath>() .map(MatchedPath::as_str); info_span!( "http_request", method = ?request.method(), matched_path, some_other_field = tracing::field::Empty, ) }) .on_request(|_request: &Request<_>, _span: &Span| { tracing::debug!("started processing request") }) .on_response(|_response: &Response, _latency: Duration, _span: &Span| { tracing::debug!("finished processing request") }) .on_failure( |_error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| { tracing::error!("something went wrong") }, ), ); let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap(); tracing::debug!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await; } async fn handler() -> Html<&'static str> { Html("<h1>Hello, World!</h1>") }需要依赖tower-http(提供TraceLayer)与tracing/tracing-subscriber。示例中MatchedPath提取器只有在axum开启matched-pathfeature 时才可用,这也是理解Router::layer运行在"路由匹配之后"的一个佐证:中间件运行时时,MatchedPath扩展已经被写入请求,可以拿到带占位符的匹配路径。
小结
Router::layer是 axum 中"为一组路由统一附加 Tower 中间件"的核心手段。使用时务必记住三条语义:中间件只作用于已注册路由(先加路由、后加 layer)、中间件运行在路由匹配之后(不能改写 URI,改写需包在 Router 外层)、中间件同样会包裹 fallback(需要"只对命中路由生效"时改用route_layer)。多个中间件优先用ServiceBuilder组合,产生错误的中间件必须配合HandleErrorLayer处理。把握好这些边界,你就能像洋葱一样精确地控制请求在路由树中的每一层处理。
【免费下载链接】axumHTTP routing and request-handling library for Rust that focuses on ergonomics and modularity项目地址: https://gitcode.com/GitHub_Trending/ax/axum
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考