ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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_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.rspub fn layerL(self, layer: L) - Self where L: LayerRoute Clone Send Sync static, L::Service: ServiceRequest Clone Send Sync static, L::Service as ServiceRequest::Response: IntoResponse static, L::Service as ServiceRequest::Error: IntoInfallible static, L::Service as ServiceRequest::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_fallbackRouter 的兜底fallback路由同样会被 Layer 包装。这带来一个容易被忽略的事实通过Router::layer添加的中间件同样会作用于请求未命中任何路由时的 fallback 处理。仓库测试 middleware_still_run_for_unmatched_requests 验证了这一点——对一个匹配不到的路径发请求计数器中间件依然被调用。方法签名与依赖约束layer的泛型约束体现了 Tower 与 axum 的对接要求L: LayerRoute Clone Send Sync staticLayer 本身必须可克隆、可跨线程发送与同步L::Service: ServiceRequest Clone Send Sync static包装后的服务需以 axum 的Request为输入Response: IntoResponse服务响应必须能转换为 HTTP 响应Error: IntoInfallible服务错误必须能转换为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否则会在合并时 panicmerge.md 中的 Panics 说明。多个中间件推荐使用 tower::ServiceBuilder当需要叠加多个中间件时文档建议使用 [tower::ServiceBuilder] 一次性组合而不是重复调用layeruse 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_uriB(req: RequestB) - RequestB { // 在这里改写 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 上还没有任何路由时调用会直接 panicPathRouter::route_layer 中routes.is_empty()检查因为它此时是无效的空操作泛型代码中可先用Router::has_routes判断。更细粒度的中间件挂载点Router::layer面向一组路由axum 还提供另外两个粒度的挂载点挂载点作用范围典型场景Router::layerRouter 中全部已有路由 fallback全局日志、超时、CORSRouter::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.mdaxum::middleware::from_fn用熟悉的async/await编写适合不打算发布成独立 crate 的中间件axum::middleware::from_extractor当你希望某个类型既能作为提取器又能作为中间件时使用tower::ServiceBuilder的map_request/map_response/then/and_then组合子适合加个响应头这类临时小改动手写tower::ServicePinBoxdyn 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_httpdebug,axum::rejectiontrace, 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() - Htmlstatic str { Html(h1Hello, 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),仅供参考
返回列表