You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 16:23:29