如何在Actix服务中间件中记录内部错误详情
我有一个actix_web服务,处理器统一返回Result<..., ServiceError>,其中ServiceError用于区分内部错误与业务错误——内部错误属于“意外”错误,详情不应对外暴露;业务错误则带有有效负载。ServiceError定义如下:
enum ServiceError { // internal error caused by being unable to open a file, etc. Internal(anyhow::Error), // HTTP error explicitly returned by the handler Message(StatusCode, String), // ...additional variants to return JSON-formatted errors and such... } impl From<anyhow::Error> for ServiceError { // provided so ? just works fn from(err: anyhow::Error) -> Self { ServiceError::Internal(err) } } impl ResponseError for ServiceError { fn status_code(&self) -> StatusCode { match self { ServiceError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, &ServiceError::Message(code, _) => code, } } fn error_response(&self) -> HttpResponse<BoxBody> { let mut builder = HttpResponseBuilder::new(self.status_code()); match self { // details of the error are hidden from end user of the API ServiceError::Internal(_) => builder.body("internal server error"), ServiceError::Message(_, msg) => builder.body(msg.clone()), } } }
我在App配置中用wrap_fn()统一记录成功响应与错误响应,不需要各处理器单独处理。现在想在发生内部错误时,在日志里记录更多详情(输出错误值),但wrap_fn()只能访问处理器生成的Actix ServiceResponse,拿不到原始的ServiceError,没法匹配ServiceError::Internal并记录对应的anyhow::Error详情。
想问actix_web有没有机制能让我观察处理器返回的实际错误类型来做日志记录?或者至少允许附加一些中间件可观察、记录的数据?
目前临时方案是在<ServiceError as ResponseError>::error_response()里记录错误,但感觉这是权宜之计,这类代码更适合放在中间件或者路由配置里。
方法1:自定义中间件捕获原始错误
Actix Web允许通过实现Service trait编写自定义中间件,直接捕获处理器返回的Result,从而拿到原始的ServiceError。示例代码如下:
use actix_web::{dev::{Service, Transform}, Error, HttpResponse}; use futures::{future::LocalBoxFuture, FutureExt}; use std::task::{Context, Poll}; // 自定义错误日志中间件 pub struct ErrorLoggingMiddleware; impl<S, B> Transform<S, actix_web::dev::ServiceRequest> for ErrorLoggingMiddleware where S: Service<actix_web::dev::ServiceRequest, Response = actix_web::dev::ServiceResponse<B>, Error = Error>, S::Future: 'static, B: 'static, { type Response = actix_web::dev::ServiceResponse<B>; type Error = Error; type Transform = ErrorLoggingMiddlewareService<S>; type InitError = (); type Future = LocalBoxFuture<'static, Result<Self::Transform, Self::InitError>>; fn new_transform(&self, service: S) -> Self::Future { async move { Ok(ErrorLoggingMiddlewareService { inner: service }) }.boxed_local() } } pub struct ErrorLoggingMiddlewareService<S> { inner: S, } impl<S, B> Service<actix_web::dev::ServiceRequest> for ErrorLoggingMiddlewareService<S> where S: Service<actix_web::dev::ServiceRequest, Response = actix_web::dev::ServiceResponse<B>, Error = Error>, S::Future: 'static, B: 'static, { type Response = actix_web::dev::ServiceResponse<B>; type Error = Error; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { self.inner.poll_ready(cx) } fn call(&self, req: actix_web::dev::ServiceRequest) -> Self::Future { let mut inner_fut = self.inner.call(req); async move { match inner_fut.await { Ok(resp) => Ok(resp), Err(err) => { // 提取原始的ServiceError并处理日志 if let Some(service_err) = err.as_error::<ServiceError>() { match service_err { ServiceError::Internal(anyhow_err) => { log::error!("Internal server error details: {:?}", anyhow_err); } ServiceError::Message(code, msg) => { log::warn!("Business error ({}): {}", code.as_u16(), msg); } } } // 继续传递错误,让ResponseError生成对外响应 Err(err) } } }.boxed_local() } }
在App中注册该中间件:
App::new() .wrap(ErrorLoggingMiddleware) // 其他路由与配置...
方法2:通过Extensions传递错误信息
如果不想编写完整中间件,可以在ServiceError的ResponseError实现中,将错误实例附加到请求的Extensions中,再在wrap_fn()里读取并记录:
首先修改error_response方法:
impl ResponseError for ServiceError { // 保留原有的status_code方法... fn error_response(&self) -> HttpResponse<BoxBody> { let mut builder = HttpResponseBuilder::new(self.status_code()); // 将错误实例存入请求扩展(需ServiceError实现Clone) if let Some(req) = builder.request() { req.extensions_mut().insert(self.clone()); } match self { ServiceError::Internal(_) => builder.body("internal server error"), ServiceError::Message(_, msg) => builder.body(msg.clone()), } } }
为ServiceError实现Clone:
#[derive(Clone)] enum ServiceError { // 原变体定义... }
然后在wrap_fn()中读取日志:
wrap_fn(|req, srv| { srv.call(req).map(|result| { match &result { Ok(resp) => { // 从请求扩展中获取错误实例并记录 if let Some(service_err) = resp.request().extensions().get::<ServiceError>() { if let ServiceError::Internal(anyhow_err) = service_err { log::error!("Internal error details: {:?}", anyhow_err); } } result } Err(_) => result, } }) })
方法3:直接在wrap_fn()中提取原始错误
Actix Web的Error类型支持通过as_error方法向下转型提取自定义错误,无需修改原有ServiceError实现,直接在wrap_fn()中处理:
wrap_fn(|req, srv| { srv.call(req).map(|result| { if let Err(err) = &result { // 提取原始ServiceError并记录日志 if let Some(service_err) = err.as_error::<ServiceError>() { match service_err { ServiceError::Internal(anyhow_err) => { log::error!("Internal server error: {:?}", anyhow_err); } ServiceError::Message(code, msg) => { log::warn!("Business error: {} - {}", code, msg); } } } } result }) })
内容的提问来源于stack exchange,提问作者user4815162342

