axum 错误处理模型与实战从 Infallible 类型系统到 HandleError 中间件【免费下载链接】axumHTTP routing and request-handling library for Rust that focuses on ergonomics and modularity项目地址: https://gitcode.com/GitHub_Trending/ax/axumaxum 作为基于 tower 的 Rust Web 框架通过类型系统保证“任何请求最终都会得到响应”而不是让错误一路冒泡到 hyper 导致连接被直接切断。本文以 axum 官方错误处理文档为核心结合 error_handling 模块源码 与仓库内多个示例系统讲解 axum 的错误处理模型、如何在 handler 中使用?、如何将可失败的 Service 与中间件接入路由以及如何让错误处理函数也使用提取器。读完本文你将掌握一套从返回错误到返回响应的完整实战方案。axum 的错误处理模型让错误无法逃逸到连接层axum 建立在tower::Service之上tower 通过 Service 的关联类型Error携带错误。若某个 Service 产生错误且该错误一路传播到 hyper连接会被直接终止且不发送任何响应——这对 Web 应用通常是不可接受的。axum 的解法很直接依靠类型系统强制所有 Service 的错误类型为Infallible永不发生的错误类型从根源上保证每个请求都一定产生响应。这意味着即使你写出这样的 handleruse axum::http::StatusCode; async fn handler() - ResultString, StatusCode { // ... }它看起来会“失败”返回StatusCode但在 axum 语义中这并不是错误。当该 handler 返回Err(StatusCode::NOT_FOUND)时这个状态码依然会通过StatusCode的IntoResponse实现被转换成Response发送给客户端。无论你返回Err(StatusCode::NOT_FOUND)还是Err(StatusCode::INTERNAL_SERVER_ERROR)在 axum 看来都是正常的响应路径不存在“错误未处理导致连接断开”的可能。这条设计贯穿 axum 的方方面面handler 的返回值、提取器的失败rejection、中间件的错误、外部 Service 的错误最终都必须落在“实现IntoResponse”这条线上。在 handler 中使用?构造中间错误类型直接返回Result_, StatusCode虽然可行但真实的 handler 往往要面对第三方库抛出的复杂错误。更实用的模式是定义自己的错误类型并为其实现IntoResponse这样就能在 handler 内部放心地使用?运算符。通用错误包装 anyhow::Error仓库中的 anyhow-error-response 示例 展示了如何用anyhow::Error统一兜底所有错误use axum::{ http::StatusCode, response::{IntoResponse, Response}, routing::get, Router, }; async fn handler() - Result(), AppError { try_thing()?; // anyhow::Error 自动转换为 AppError Ok(()) } fn try_thing() - Result(), anyhow::Error { anyhow::bail!(it failed!) } // 自定义错误包装 anyhow::Error struct AppError(anyhow::Error); // 告诉 axum 如何把 AppError 转换成响应 impl IntoResponse for AppError { fn into_response(self) - Response { ( StatusCode::INTERNAL_SERVER_ERROR, format!(Something went wrong: {}, self.0), ) .into_response() } } // 让 ? 能把任意可转成 anyhow::Error 的错误自动变成 AppError implE FromE for AppError where E: Intoanyhow::Error, { fn from(err: E) - Self { Self(err.into()) } } fn app() - Router { Router::new().route(/, get(handler)) }该示例的测试代码用tower::ServiceExt::oneshot直接驱动app()断言失败请求返回500 INTERNAL_SERVER_ERROR且响应体为Something went wrong: it failed!完整验证了“错误 → 响应”的转换链路。运行方式为cargo run -p example-anyhow-error-response。应用级错误细分错误并控制对外暴露如果需要对错误做细分例如区分用户输入错误与内部错误可以参考 error-handling 示例。它定义了枚举AppError并针对每种变体决定是否向客户端暴露细节#[derive(Debug)] enum AppError { JsonRejection(JsonRejection), // 请求体 JSON 非法 TimeError(time_library::Error), // 第三方库错误 } impl IntoResponse for AppError { fn into_response(self) - Response { #[derive(Serialize)] struct ErrorResponse { message: String, } let (status, message, err) match self { AppError::JsonRejection(rejection) { // 用户输入导致的错误不记录日志直接返回其状态码与正文 (rejection.status(), rejection.body_text(), None) } AppError::TimeError(_err) { // 内部错误不向客户端暴露任何细节 ( StatusCode::INTERNAL_SERVER_ERROR, Something went wrong.to_owned(), Some(self), // 将错误作为 Extension 塞进响应 ) } }; let mut response (status, AppJson(ErrorResponse { message })).into_response(); if let Some(err) err { response.extensions_mut().insert(Arc::new(err)); } response } }这里的技巧值得注意内部错误通过response.extensions_mut().insert(Arc::new(err))放入响应扩展中随后由log_app_errors中间件检测到该 Extension 时用tracing::error!记录完整错误从而做到“客户端只见笼统文案、服务端完整留痕”async fn log_app_errors(request: Request, next: Next) - Response { let response next.run(request).await; if let Some(err) response.extensions().get::ArcAppError() { tracing::error!(?err, an unexpected error occurred inside a handler); } response }同时该示例用#[from_request(via(axum::Json), rejection(AppError))]派生自己的AppJson提取器把 JSON 解析失败也统一收敛进AppError并配合impl FromJsonRejection for AppError与impl Fromtime_library::Error for AppError让?自动完成转换。运行方式为cargo run -p example-error-handling其注释中还给出了成功200与失败500两种请求的tower_http::trace日志样例可直接对照验证。提取器的拒绝Rejection同样是响应这一模型同样适用于提取器。如果提取器无法匹配请求请求会被拒绝并直接返回响应而不会调用你的 handler。因此提取器的失败类型rejection也要求能转换成Response。关于如何处理提取器失败详见 extract 模块文档常用手段包括用ResultT, T::Rejection作为提取器参数在 handler 内部按需处理例如区分JsonRejection::MissingJsonContentType、JsonRejection::JsonDataError、JsonRejection::JsonSyntaxError等变体通过自定义提取器覆盖 rejection 响应仓库中的 customize-extractor-error 示例 提供了三种实现路径手写FromRequest包装custom_extractor.rs、使用axum_extra::extract::WithRejection转换 rejectionwith_rejection.rs、以及用#[derive(FromRequest)]的via(...)/rejection(...)属性派生derive_from_request.rs。路由到可失败的 Service使用 HandleError如果只用 async 函数作为 handler通常不需要关心错误类型。但当你嵌入通用的Service或叠加可能产生错误的中间件时就必须告诉 axum 如何把这些错误转换为响应因为 axum 要求最终的错误类型为Infallible。官方文档给出的场景是某个基于tower::service_fn的服务可能以anyhow::Error失败不能直接挂到路由上use axum::{ Router, body::Body, http::{Request, Response, StatusCode}, error_handling::HandleError, }; async fn thing_that_might_fail() - Result(), anyhow::Error { // ... Ok(()) } // 这个 service 可能以 anyhow::Error 失败 let some_fallible_service tower::service_fn(|_req| async { thing_that_might_fail().await?; Ok::_, anyhow::Error(Response::new(Body::empty())) }); let app Router::new().route_service( /, // 不能直接路由到 some_fallible_service因为它可能失败。 // 必须用 handle_error 把错误转换成响应 // 同时把错误类型从 anyhow::Error 变成 Infallible。 HandleError::new(some_fallible_service, handle_anyhow_error), ); // 把错误转换成实现了 IntoResponse 的类型 async fn handle_anyhow_error(err: anyhow::Error) - (StatusCode, String) { ( StatusCode::INTERNAL_SERVER_ERROR, format!(Something went wrong: {err}), ) }HandleError是一个Service适配器内部调用被包装的 service成功时把响应.into_response()失败时调用你提供的错误处理函数f(err)并把其结果.into_response()。从 HandleError 的 Service 实现 可以看到它的type Error Infallible;call内部大致等价于match inner.oneshot(req).await { Ok(res) Ok(res.into_response()), Err(err) Ok(f(err).await.into_response()), }这正好从源码层面印证了文档中“错误被转换为响应”的表述。应用可失败的中间件HandleErrorLayer对中间件错误axum 要求使用HandleErrorLayer。它是一个Layer将其内部Service包上HandleError。最经典的场景是配合tower::timeout超时会产生错误必须被处理use axum::{ Router, BoxError, routing::get, http::StatusCode, error_handling::HandleErrorLayer, }; use std::time::Duration; use tower::ServiceBuilder; let app Router::new() .route(/, get(|| async {})) .layer( ServiceBuilder::new() // timeout 会在 handler 处理过慢时产生错误所以必须处理 .layer(HandleErrorLayer::new(handle_timeout_error)) .timeout(Duration::from_secs(30)) ); async fn handle_timeout_error(err: BoxError) - (StatusCode, String) { if err.is::tower::timeout::error::Elapsed() { ( StatusCode::REQUEST_TIMEOUT, Request took too long.to_string(), ) } else { ( StatusCode::INTERNAL_SERVER_ERROR, format!(Unhandled internal error: {err}), ) } }要点拆解错误处理函数接收BoxErrortower 错误的通用擦除类型可用err.is::T()判断具体错误类型从而区分“超时408”与“其他内部错误500”ServiceBuilder中层的顺序决定调用顺序HandleErrorLayer必须放在可能产生错误的层如timeout之前内层确保它能捕获其外层产生的错误从HandleErrorLayer的Layer实现源码可见它只是HandleError::new(inner, self.f.clone())的包装错误处理函数F需要Clone这与Service每次call都会克隆处理函数的行为一致见 HandleError::call。在错误处理中运行提取器HandleErrorLayer还支持在错误处理函数中运行提取器这让错误响应可以携带请求上下文。文档给出的写法是提取器作为参数放在前面最后一个参数必须是错误本身use axum::{ Router, BoxError, routing::get, http::{StatusCode, Method, Uri}, error_handling::HandleErrorLayer, }; use std::time::Duration; use tower::ServiceBuilder; let app Router::new() .route(/, get(|| async {})) .layer( ServiceBuilder::new() .layer(HandleErrorLayer::new(handle_timeout_error)) .timeout(Duration::from_secs(30)) ); async fn handle_timeout_error( // Method 和 Uri 是提取器可以在这里使用 method: Method, uri: Uri, // 最后一个参数必须是错误本身 err: BoxError, ) - (StatusCode, String) { ( StatusCode::INTERNAL_SERVER_ERROR, format!({method} {uri} failed with {err}), ) }源码层面的支撑在于HandleError通过宏为带提取器的变体生成了独立的Service实现见 impl_service! 宏与调用最多支持16 个提取器参数。其call内部先执行let $ty match $ty::from_request_parts(mut parts, ()).await { Ok(value) value, Err(rejection) return Ok(rejection.into_response()), };也就是说这些提取器在请求尚未交给被包装的 service 之前先从Request的 parts 中提取如Method、Uri、HeaderMap等不消费请求体的类型若提取本身失败则直接返回该 rejection 转换成的响应若提取成功且内部 service 出错才调用f($ty, ..., err)。注意由于提取发生在请求消费之前这里只能使用实现FromRequestParts的提取器——消费请求体的提取器如Json、String不能用于该场景。源码视角HandleError 与 HandleErrorLayer 的设计总结从 error_handling 模块 的完整实现可以提炼出 axum 错误处理的底层事实错误类型归一HandleErrorS, F, T的type Error Infallible源码无论内部 service 以何种错误失败对外都以“永不失败”的 Service 形态接入路由从而满足 axum 的全局约束。Trait 泛型约束S::Response: IntoResponse、Res: IntoResponse即内部 service 的响应与错误处理函数的返回值都必须能转成Response源码。提取器支持带提取器的变体要求每个$ty: FromRequestParts() Send提取失败时返回rejection.into_response()源码。层与适配器分离HandleErrorLayerF, T负责在ServiceBuilder栈中把下层 service 包成HandleErrorS, F, TT通过PhantomData记录提取器元组类型不参与运行时行为。实践建议与适用边界纯 handler 应用优先使用“自定义错误类型 impl IntoResponseFrom自动转换”的模式handler 内可放心使用?见 error-handling 示例 与 anyhow-error-response 示例。接入第三方/自建 Service用HandleError::new(service, handler)包裹后通过route_service挂载。叠加会产生错误的中间件如timeout、限流、重试等用HandleErrorLayer放在ServiceBuilder对应层的内侧错误处理函数中可用err.is::T()或downcast_ref区分错误种类。需要请求上下文错误处理函数的参数可声明Method、Uri等FromRequestParts提取器最后一个参数必须是错误本身最多支持 16 个提取器参数。保留原始错误如需在日志中记录完整内部错误可参考 error-handling 示例 的做法把错误以Arc形式塞进响应 Extension由日志中间件读取避免在IntoResponse中产生副作用。axum 的错误处理哲学始终如一错误不是终点响应才是。通过Infallible的类型约束与IntoResponse的响应转换axum 把“每个请求都必须有响应”这一运行时要求提前到了编译期开发者只需专注于“如何把错误映射成响应”剩下的交给类型系统保证。【免费下载链接】axumHTTP routing and request-handling library for Rust that focuses on ergonomics and modularity项目地址: https://gitcode.com/GitHub_Trending/ax/axum创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考 SEO 优化官网定制响应式建站教育培训建站