news 2026/9/10 13:54:39

axum 路由层中间件指南:用 Router::layer 为整组路由统一添加 Tower 中间件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
axum 路由层中间件指南:用 Router::layer 为整组路由统一添加 Tower 中间件

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_layerMethodRouter::layerHandler::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::layerRouter 中全部已有路由 + 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::ServiceBuildermap_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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!