「这是我参与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_web 和 tracing_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),
}
}
}
ResponseError 是 actix_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则向我们提供遇到的失败的简要描述,并提供基本的上下文量。