「深挖Rust」错误处理 — 4

353 阅读4分钟

「这是我参与2022首次更文挑战的第 5 天,活动详情查看:2022首次更文挑战」。


跟踪错误的根本原因

为了理解为什么 tracing_actix_web 的日志记录如此糟糕,我们需要检查(再次)我们的请求处理程序和 store_token

//! src/routes/subscriptions.rs
// [...]

pub async fn subscribe(/* */) -> HttpResponse {
    // [...]
    if store_token(&mut transaction, subscriber_id, &subscription_token)
        .await
        .is_err()
    {
        return HttpResponse::InternalServerError().finish();
    }
    // [...]
}

pub async fn store_token(/* */) -> Result<(), sqlx::Error> {
    sqlx::query!(/* */)
        .execute(transaction)
        .await
        .map_err(|e| {
            tracing::error!("Failed to execute query: {:?}", e);
            e
        })?;
    // [...]
}

我们上面看到的关于数据库的错误日志实际上是由那个 tracing::error 调用产生的。错误信息包括由 execute 返回的 sqlx::Error。 我们使用 ? 操作符向上抛出错误,但在 subscribe 中调用链断开了:我们失去从 store_token 接收到的错误,随即创建了一个 500 响应返回了。

actix_webtracing_actix_web::TracingLogger 准备发出各自的日志记录时,HttpResponse::InternalServerError().finish() 是唯一可以访问的东西。而这个错误不包含任何关于根本原因的追踪上下文。因此,日志记录同样没有用。

我们如何解决这个问题呢?下面我们开始利用 actix_web 提供的错误处理机制 → 特别是actix_web::Error

根据文档的内容:actix_web::Error 以一种更为方便的方式将 std::error 中的枚举错误带入actix_web。

这听起来就像我们正在寻找的东西。我们如何建立 actix_web::Error 的实例?

通过用 into() 转换错误来创建 actix_web::Error

不太直接,但我们可以想个办法。浏览文档中列出的唯一的 From/Into 实现,我们可以使用的似乎只有这一个:

/// Build an `actix_web::Error` from any error that implements `ResponseError`
impl<T: ResponseError + 'static> From<T> for Error {
    fn from(err: T) -> Error {
        Error {
            cause: Box::new(err),
        }
    }
}

ResponseErroractix_web 暴露的一个 Trait

/// Errors that can be converted to `Response`.
pub trait ResponseError: fmt::Debug + fmt::Display {
    /// Response's status code.
    ///
    /// The default implementation returns an internal server error.
    fn status_code(&self) -> StatusCode;

    /// Create a response from the error.
    ///
    /// The default implementation returns an internal server error.
    fn error_response(&self) -> Response;
}

我们现在只需要为我们的错误实现它就可以了。

actix_web 为这两个方法提供了一个默认的实现,返回 500 Internal Server Error — 这正是我们需要的。因此,只需要写:

//! src/routes/subscriptions.rs
use actix_web::ResponseError;
// [...]

impl ResponseError for sqlx::Error {}

但是编译器却不接受这种写法:

error[E0117]: only traits defined in the current crate
              can be implemented for arbitrary types
   --> src/routes/subscriptions.rs:162:1
    |
162 | impl ResponseError for sqlx::Error {}
    | ^^^^^^^^^^^^^^^^^^^^^^^-----------
    | |                      |
    | |                      `sqlx::Error` is not defined in the current crate
    | impl doesn't use only types from inside the current crate
    |
    = note: define and implement a trait or new type instead

这里我们触发了 Rust的孤儿规则 :禁止为一个外来类型实现一个外来特性,*这里的外来代表“来自另一个crate(包括自己编写的crate)” *。

这个限制是为了保持一致性:想象一下,如果你添加了一个依赖,它为 sqlx::Error 定义了自己的 ResponseError 实现。当trait方法被调用时,编译器应该使用哪个?

抛开孤儿规则不提,为 sqlx::Error 实现 ResponseError 仍然是一个不对的做法。

当试图持久化 subscriber token 时,遇到 sqlx::Error 时,我们希望返回一个 500 Internal Server Error。在另一种情况下,我们可能希望以不同的方式处理 sqlx::Error

我们应该遵循编译器的建议:定义一个新的类型来包装 sqlx::Error

//! src/routes/subscriptions.rs
// [...]

//                                    Using the new error type!
pub async fn store_token(/* */) -> Result<(), StoreTokenError> {
    sqlx::query!(/* */)
    .execute(transaction)
    .await
    .map_err(|e| {
        // [...]
        // Wrapping the underlying error
        StoreTokenError(e)
    })?;
    // [...]
}

// A new error type, wrapping a sqlx::Error
pub struct StoreTokenError(sqlx::Error);

impl ResponseError for StoreTokenError {}

依然没有作用,但原因不同:

error[E0277]: `StoreTokenError` doesn't implement `std::fmt::Display`
   --> src/routes/subscriptions.rs:164:6
    |
164 | impl ResponseError for StoreTokenError {}
    |      ^^^^^^^^^^^^^
    `StoreTokenError` cannot be formatted with the default formatter
    |
    |
59  | pub trait ResponseError: fmt::Debug + fmt::Display {
    |                                       ------------
    |			required by this bound in `ResponseError`
    |
    = help: the trait `std::fmt::Display` is not implemented for `StoreTokenError`

error[E0277]: `StoreTokenError` doesn't implement `std::fmt::Debug`
   --> src/routes/subscriptions.rs:164:6
    |
164 | impl ResponseError for StoreTokenError {}
    |      ^^^^^^^^^^^^^
    `StoreTokenError` cannot be formatted using `{:?}`
    |
    |
59  | pub trait ResponseError: fmt::Debug + fmt::Display {
    |                          ----------
                required by this bound in `ResponseError`
    |
    = help: the trait `std::fmt::Debug` is not implemented for `StoreTokenError`
    = note: add `#[derive(Debug)]` or manually implement `std::fmt::Debug`

我们缺少对 StoreTokenError 的两个 Trait 实现: Debug & Display。这两个 Trait 都与格式化有关,但它们的目的不同。

Debug应该返回一个面向程序员的表示,尽可能贴合底层类型结构,以帮助调试(正如其名称所暗示的)。几乎所有的公共类型都应该实现Debug。

相反,Display应该返回一个面向用户的底层类型的表示。大多数类型都没有实现Display,它不能通过 #[derive(Display)] 自动实现。

在处理错误时,我们可以推断出以下两个特征: Debug返回尽可能多的信息,而Display则向我们提供遇到的失败的简要描述,并提供基本的上下文量。