ARTICLE DETAIL

资讯详情

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

axum 中间件完全指南:基于 tower 的中间件架构、执行顺序与错误处理实战

axum 中间件完全指南:基于 tower 的中间件架构、执行顺序与错误处理实战 axum 中间件完全指南基于 tower 的中间件架构、执行顺序与错误处理实战【免费下载链接】axumHTTP routing and request-handling library for Rust that focuses on ergonomics and modularity项目地址: https://gitcode.com/GitHub_Trending/ax/axum导读本文以 axum/src/docs/middleware.md 为核心文档系统讲解 axum 中间件体系的完整设计从为什么 axum 没有自研中间件系统的设计哲学到Router::layer/route_layer/Handler::layer的挂载方式再到中间件洋葱模型与tower::ServiceBuilder组合顺序最后深入from_fn、from_extractor、手写tower::Service四种编写路径及错误处理、状态访问、URI 重写等高频实战问题。读完本文你将掌握在 axum 应用中正确选择、编排、编写中间件并理解其底层执行语义。一、设计哲学axum 没有自己的中间件系统axum 的独特之处在于它没有自研一套专属中间件体系而是直接与 [tower] 深度集成。这意味着tower生态与tower-http生态中的全部中间件日志、CORS、压缩、超时、限流等都可以开箱即用地接入 axum无需任何适配层。从源码结构看这一设计贯穿整个仓库axum/src/middleware/mod.rs 中的模块文档直接include_str!(../docs/middleware.md)即本文核心文档就是该模块的官方指南模块对外导出的from_fn、from_fn_with_state、from_extractor、map_request、map_response等工具全部实现为tower::Layer/tower::Service组合而非自造抽象。虽然使用 axum 中间件不要求完全理解 tower但官方建议至少掌握 tower 的基础概念可参考 tower 官方的 guides 系列并通读 [tower::ServiceBuilder] 的文档——因为ServiceBuilder是组合多个中间件的事实标准工具。二、在哪里挂载中间件四种入口axum 允许你在几乎任何位置添加中间件挂载位置API作用范围整个路由树Router::layer、Router::route_layer包裹整个Router或其中所有已注册路由方法路由器MethodRouter::layer、MethodRouter::route_layer包裹单个MethodRouter如某个get(...)链单个处理器Handler::layer只包裹单个 handler返回Layered包装2.1Router::layer包裹整个路由树从 Router::layer 的实现 可以看到它对内部path_router和catch_all_fallback分别应用layer从而覆盖路由器中的全部既有路由。使用它时有两个关键注意点参见 axum/src/docs/routing/layer.md只作用于已存在的路由必须先添加路由及 fallback再调用layer之后再新增的路由不会带上该中间件。运行时机在路由匹配之后Router::layer的中间件无法重写请求 URI详见本文第八节。2.2Router::route_layer仅对匹配到的路由生效route_layer与layer的差别在于中间件只有在请求匹配到某个路由时才会运行。这特别适合授权类中间件——否则一个本应返回404 Not Found的请求可能因为全局中间件提前return Err(...)而变成401 Unauthorized掩盖了路由不存在这一事实。route_layer的文档示例axum/src/docs/routing/route_layer.md验证了这一行为GET /foo 有效 token →200 OKGET /foo 无效 token →401 UnauthorizedGET /not-found 无效 token →404 Not Found中间件不运行另外注意route_layer在路由器还没有声明任何路由时会panic因为此时该 layer 不会产生任何效果这通常是个 bug泛型代码中可先用Router::has_routes判断。2.3 仅对部分路由应用中间件用merge组合如果只想让部分路由带上中间件官方推荐先用各自的路由器挂载中间件再通过Router::merge合并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()); // Merge everything into one Router let app Router::new() .merge(with_tracing) .merge(with_compression);三、一次挂载多个中间件优先使用ServiceBuilder当需要同时应用多个中间件时官方强烈建议使用tower::ServiceBuilder一次性组合而不是反复调用layer/route_layeruse axum::{ routing::get, Extension, Router, }; use tower_http::{trace::TraceLayer}; use tower::ServiceBuilder; 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 {})) ); # let _: Router app;这样做的原因与执行顺序密切相关见下一节ServiceBuilder会把所有 layer 组合成一个并按从上到下的顺序执行更符合人类直觉。四、执行顺序洋葱模型与两种组合方向4.1 多次调用layer从下往上执行当你用Router::layer或同类方法依次添加中间件时所有先前添加的路由都会被新中间件包裹从执行效果上看中间件自底向上运行use axum::{routing::get, Router}; async fn handler() {} let app Router::new() .route(/, get(handler)) .layer(layer_one) .layer(layer_two) .layer(layer_three);可以把它想象成一个洋葱——每一层新中间件都包裹住前面所有层requests | v ----- layer_three ----- | ---- layer_two ---- | | | -- layer_one -- | | | | | | | | | | | handler | | | | | | | | | | | -- layer_one -- | | | ---- layer_two ---- | ----- layer_three ----- | v responses即请求的完整链路是layer_three→layer_two→layer_one→handler然后响应原路返回layer_one→layer_two→layer_three。需要注意这只是便于理解的思维模型。实际上任何中间件都可以提前短路返回例如请求未通过授权时直接返回响应而不调用下一层此时响应不会经过更内层的中间件。4.2ServiceBuilder从上往下执行同样是三个 layer如果改用ServiceBuilder组合use tower::ServiceBuilder; use axum::{routing::get, Router}; let app Router::new() .route(/, get(handler)) .layer( ServiceBuilder::new() .layer(layer_one) .layer(layer_two) .layer(layer_three), );ServiceBuilder会把所有 layer 合成一个执行顺序变为从上到下请求先到达layer_one再layer_two、layer_three最后进入handler响应再按layer_three→layer_two→layer_one的顺序冒泡返回。从上到下的执行顺序在心理上更容易跟踪和推理这也是官方推荐ServiceBuilder的重要原因之一。五、常用中间件速查tower-http 生态既然 axum 直接复用 tower 生态以下中间件即可直接使用中间件模块用途TraceLayertower_http::trace高层级的 tracing / 日志CorsLayertower_http::cors处理跨域 CORSCompressionLayertower_http::compression对响应自动压缩RequestIdLayer/PropagateRequestIdLayertower_http::request_id设置与传播请求 IDTimeoutLayertower_http::timeout::TimeoutLayer请求超时控制六、编写自己的中间件四条路径与取舍axum 提供了多种编写中间件的方式抽象层级不同各有优劣。6.1axum::middleware::from_fnasync/await 风格最易上手使用axum::middleware::from_fn编写中间件适合不想手写 Future习惯使用熟悉的async/await语法不打算把中间件发布成 crate 供他人使用此类中间件只兼容 axum。从 from_fn 的实现 可以确认其函数签名约束必须是一个async fn可以接收零个或多个实现FromRequestParts的提取器如HeaderMap倒数第二个参数必须是恰好一个实现FromRequest的提取器Request满足最后一个参数必须是Next返回值需实现IntoResponse。典型的认证中间件写法use axum::{ Router, http::{StatusCode, HeaderMap}, middleware::{self, Next}, response::Response, extract::Request, routing::get, }; async fn auth( headers: HeaderMap, // FromRequestParts 提取器 request: Request, // 最后一个提取器实现 FromRequest next: Next, // 最后一个参数 ) - ResultResponse, StatusCode { match get_token(headers) { Some(token) if token_is_valid(token) { let response next.run(request).await; Ok(response) } _ Err(StatusCode::UNAUTHORIZED), } } let app Router::new() .route(/, get(|| async { /* ... */ })) .route_layer(middleware::from_fn(auth));源码实现细节值得留意中间件内可自由使用提取器比如HeaderMap、Request提取失败时会把 rejection 直接into_response()Next内部持有BoxCloneSyncService其run方法的错误类型是Infallible见 from_fn.rs 中 Next 的定义因此调用方无需处理错误分支。6.2axum::middleware::from_extractor提取器即中间件使用axum::middleware::from_extractor适合那种有时当提取器、有时当中间件的类型。如果某个类型只打算当中间件用则优先选择from_fn。其语义见 from_extractor.rs 的文档提取器成功则丢弃值、继续调用内部服务提取失败则直接返回 rejection内部服务不会被调用。典型场景是写一个RequireAuth提取器做授权校验然后通过route_layer一次性保护多个路由use axum::{ extract::FromRequestParts, middleware::from_extractor, routing::{get, post}, Router, http::{header, StatusCode, request::Parts}, }; struct RequireAuth; implS FromRequestPartsS for RequireAuth where S: Send Sync, { type Rejection StatusCode; async fn from_request_parts(parts: mut Parts, state: S) - ResultSelf, Self::Rejection { let auth_header parts .headers .get(header::AUTHORIZATION) .and_then(|value| value.to_str().ok()); match auth_header { Some(auth_header) if token_is_valid(auth_header) Ok(Self), _ Err(StatusCode::UNAUTHORIZED), } } } let app Router::new() .route(/, get(handler)) .route(/foo, post(other_handler)) // 提取器会在所有路由之前运行 .route_layer(from_extractor::RequireAuth());⚠️ 注意如果提取器会消费请求体如String、Bytes原地会留下空 body后续提取器或 handler 将无法再读取请求体。6.3 tower 组合子轻量请求/响应修改tower 提供若干工具组合子适合做加个头这类小而临时的操作ServiceBuilder::map_requestServiceBuilder::map_responseServiceBuilder::thenServiceBuilder::and_then适用场景想执行一个小的即席操作如添加响应头且不打算发布为通用 crate。6.4 手写tower::ServicePinBoxdyn Future最大控制力需要最大控制力以及更底层的 API时可以直接实现tower::Service。官方给出了完整模板原样继承use axum::{ response::Response, body::Body, extract::Request, }; use futures_core::future::BoxFuture; use tower::{Service, Layer}; use std::task::{Context, Poll}; #[derive(Clone)] struct MyLayer; implS LayerS for MyLayer { type Service MyMiddlewareS; fn layer(self, inner: S) - Self::Service { MyMiddleware { inner } } } #[derive(Clone)] struct MyMiddlewareS { inner: S, } implS ServiceRequest for MyMiddlewareS where S: ServiceRequest, Response Response Send static, S::Future: Send static, { type Response S::Response; type Error S::Error; // BoxFuture 是 PinBoxdyn Future Send a 的类型别名 type Future BoxFuturestatic, ResultSelf::Response, Self::Error; fn poll_ready(mut self, cx: mut Context_) - PollResult(), Self::Error { self.inner.poll_ready(cx) } fn call(mut self, request: Request) - Self::Future { let future self.inner.call(request); Box::pin(async move { let response: Response future.await?; Ok(response) }) } }使用这种方式的时机中间件需要可配置例如通过tower::Layer上的 builder 方法如tower_http::trace::TraceLayer的配置项打算把中间件发布成 crate 供他人使用不习惯或不想手写自己的 Future。关键设计原则错误类型被定义为S::Error意味着你的中间件通常不产生错误。原则是尽量总是返回一个响应不要用自定义错误类型中途退出。例如第三方库返回了专用错误类型应将其转换为合理的响应并返回Ok(该响应)。如果你确实实现了自定义错误类型如type Error BoxError或任何非Infallible的类型则必须配合HandleErrorLayer将错误转换为响应ServiceBuilder::new() .layer(HandleErrorLayer::new(|_: BoxError| async { // 因为 axum 使用 infallible 错误你必须在这里处理中间件返回的自定义错误 StatusCode::BAD_REQUEST })) .layer( // 你的真正会返回错误的 layer );6.5 手写tower::Service 自定义 Future极致性能如果熟悉或想学习手写 Future并且需要尽可能多的控制力可以使用不带 boxed future 的tower::Service追求最低开销中间件需要可配置打算发布为 crate甚至作为 tower-http 的一部分熟悉 async Rust 底层机制。tower 官方的Building a middleware from scratch指南是学习这一路径的最佳起点。七、中间件的错误处理HandleErrorLayeraxum 的错误处理模型要求每个 handler 必须始终返回响应。但中间件是应用引入错误的可能途径之一如果错误一路传到 hyper连接会被直接关闭而不发送任何响应。因此 axum 要求中间件产生的错误必须被优雅处理。典型做法是用HandleErrorLayer把错误转成响应。注意HandleErrorLayer必须放在会产生错误的中间件之上因为只有它才能接收到下层返回的错误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 之上因为它要接收 // TimeoutLayer 返回的错误 .layer(HandleErrorLayer::new(|_: BoxError| async { StatusCode::REQUEST_TIMEOUT })) .layer(TimeoutLayer::new(Duration::from_secs(10))) );axum 错误处理模型的完整细节参见 axum/src/docs/error_handling.mdaxum 通过类型系统强制所有服务错误类型为Infallible即便 handler 返回ResultString, StatusCodeErr也会被StatusCode的IntoResponse实现转成响应发回客户端而不被视为错误。HandleErrorLayer还支持运行提取器如Method、Uri最后一个参数是错误本身便于构造更丰富的错误响应。八、路由到服务/中间件与背压backpressure将请求路由到多个服务之一与背压天生不兼容理想情况下你应该先确认服务就绪再调用它但要确定调用哪个服务你首先得拿到请求……这构成了矛盾。业界有两种解法等所有目标服务都就绪路由器才就绪——这是tower::steer::Steer采用的方案始终认为所有服务就绪Service::poll_ready恒返回Poll::Ready(Ok(()))把真正的就绪检查推迟到Service::call返回的响应 future 内部去驱动——适用于不在乎背压、总是就绪的服务。axum 假定应用中的所有服务都不关心背压因此采用第二种策略。由此带来的约束应避免路由到或使用关心背压的服务/中间件至少要配合tower::load_shed快速丢弃请求避免积压。如果poll_ready返回错误该错误会在call的响应 future 中返回而不是在poll_ready中返回此时底层服务不会被丢弃仍会用于后续请求。期望poll_ready 失败即被丢弃的服务不应与 axum 搭配使用。可行的折衷把背压敏感的中间件包在整个应用外面。由于 axum 应用本身就是tower::Service可以直接用ServiceBuilder包裹use axum::{ routing::get, Router, }; use tower::ServiceBuilder; async fn handler() { /* ... */ } let app Router::new().route(/, get(handler)); let app ServiceBuilder::new() .layer(some_backpressure_sensitive_middleware) .service(app);但这样包裹整个应用时要确保错误仍然被妥善处理。另外由 async 函数创建的 handler 不关心背压、始终就绪——如果你没用任何 tower 中间件则完全无需担心上述问题。九、在中间件中访问状态State如何让中间件拿到状态取决于中间件的编写方式。9.1from_fn类中间件直接使用axum::middleware::from_fn_with_state即可让from_fn中间件提取State。注意普通from_fn不支持提取State这是两者最直观的区别。9.2 自定义tower::Layer中访问状态在自定义 Layer/Service 中把状态克隆进服务结构体即可完整模板use axum::{ Router, routing::get, middleware::{self, Next}, response::Response, extract::{State, Request}, }; use tower::{Layer, Service}; use std::task::{Context, Poll}; #[derive(Clone)] struct AppState {} #[derive(Clone)] struct MyLayer { state: AppState, } implS LayerS for MyLayer { type Service MyServiceS; fn layer(self, inner: S) - Self::Service { MyService { inner, state: self.state.clone(), } } } #[derive(Clone)] struct MyServiceS { inner: S, state: AppState, } implS, B ServiceRequestB for MyServiceS where S: ServiceRequestB, { type Response S::Response; type Error S::Error; type Future S::Future; fn poll_ready(mut self, cx: mut Context_) - PollResult(), Self::Error { self.inner.poll_ready(cx) } fn call(mut self, req: RequestB) - Self::Future { // 在这里使用 self.state // 可参考 axum::RequestExt 了解如何直接从 Request 运行提取器 self.inner.call(req) } } async fn handler(_: StateAppState) {} let state AppState {}; let app Router::new() .route(/, get(handler)) .layer(MyLayer { state: state.clone() }) .with_state(state);挂载时通过.with_state(state)完成最终状态注入with_state的实现见 axum/src/routing/mod.rs它把状态同时分发到path_router与 fallback 路由。十、从中间件向 handler 传递数据请求扩展Extensions中间件与 handler 之间可以通过**请求扩展request extensions**传递数据。经典案例认证中间件把CurrentUser塞进请求扩展handler 再用Extension提取器取回use axum::{ Router, http::StatusCode, routing::get, response::{IntoResponse, Response}, middleware::{self, Next}, extract::{Request, Extension}, }; #[derive(Clone)] struct CurrentUser { /* ... */ } async fn auth(mut req: Request, next: Next) - ResultResponse, StatusCode { let auth_header req.headers() .get(http::header::AUTHORIZATION) .and_then(|header| header.to_str().ok()); let auth_header if let Some(auth_header) auth_header { auth_header } else { return Err(StatusCode::UNAUTHORIZED); }; if let Some(current_user) authorize_current_user(auth_header).await { // 把当前用户插入请求扩展供 handler 提取 req.extensions_mut().insert(current_user); Ok(next.run(req).await) } else { Err(StatusCode::UNAUTHORIZED) } } async fn authorize_current_user(auth_token: str) - OptionCurrentUser { // ... unimplemented!() } async fn handler( // 提取中间件设置的用户信息 Extension(current_user): ExtensionCurrentUser, ) { // ... } let app Router::new() .route(/, get(handler)) .route_layer(middleware::from_fn(auth));注意响应扩展response extensions也可以使用但请求扩展不会自动迁移到响应扩展如果需要你必须手动为所需扩展完成迁移。十一、在中间件中重写请求 URI绕开路由已匹配的限制通过Router::layer添加的中间件在路由匹配之后才运行因此它无法用于重写请求 URI这类必须在路由决策之前发生的操作。解决方法把中间件包在整个Router外面Router实现了tower::Service所以可以这样做use tower::Layer; use axum::{ Router, ServiceExt, // 提供 into_make_service response::Response, middleware::Next, extract::Request, }; fn rewrite_request_uriB(req: RequestB) - RequestB { // ... req } // 可以是任意 tower::Layer let middleware tower::util::MapRequestLayer::new(rewrite_request_uri); let app Router::new(); // 把 layer 包在整个 Router 外 // 这样中间件会在 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;这样处理之后URI 重写发生在路由匹配之前Router才能基于改写后的 URI 完成正确的路由分发。十二、更多参考中间件模块全部导出见 axum/src/middleware/mod.rs包括from_fn、from_fn_with_state、from_extractor、map_request、map_response及其 Layer/Service/响应 Future 类型Router::layer与Router::route_layer的详细文档见 axum/src/docs/routing/layer.md 与 axum/src/docs/routing/route_layer.mdaxum 错误处理模型的整体说明见 axum/src/docs/error_handling.md中间件与错误处理的完整可运行示例可参考仓库examples/下的error-handling、consume-body-in-extractor-or-middleware、request-id等目录。【免费下载链接】axumHTTP routing and request-handling library for Rust that focuses on ergonomics and modularity项目地址: https://gitcode.com/GitHub_Trending/ax/axum创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表