「这是我参与2022首次更文挑战的第 9 天,活动详情查看:2022首次更文挑战」。
错误类型不丰富
那我们的日志呢? 让我们再看看。
export RUST_LOG="sqlx=error,info"
export TEST_LOG=enabled
cargo t subscribe_fails_if_there_is_a_fatal_database_error | bunyan
...
INFO: [HTTP REQUEST - END]
exception.details="StoreTokenError(
A database failure was encountered while trying to
store a subscription token.
Caused by:
error returned from database: column 'subscription_token'
of relation 'subscription_tokens' does not exist)"
exception.message="Failed to create a new subscriber.",
target=tracing_actix_web::root_span_builder,
http.status_code=500
我们仍然在exception.details中得到了底层 StoreTokenError 很好的表示,但它显示我们现在正在使用 SubscribeError 的 #[derive(Debug)] 实现。虽然没有损失任何信息。
而exception.message就不一样了。不管是什么故障模式,我们总是得到 Failed to create a new subscriber。这不是很明确。
让我们细化我们的Debug和Display实现。
//! src/routes/subscriptions.rs
// [...]
// Remember to delete `#[derive(Debug)]`!
impl std::fmt::Debug for SubscribeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self, f)
}
}
impl std::error::Error for SubscribeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
// &str does not implement `Error` - we consider it the root cause
SubscribeError::ValidationError(_) => None,
SubscribeError::DatabaseError(e) => Some(e),
SubscribeError::StoreTokenError(e) => Some(e),
SubscribeError::SendEmailError(e) => Some(e),
}
}
}
impl std::fmt::Display for SubscribeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SubscribeError::ValidationError(e) => write!(f, "{}", e),
// What should we do here?
SubscribeError::DatabaseError(_) => write!(f, "???"),
SubscribeError::StoreTokenError(_) => write!(
f,
"Failed to store the confirmation token for a new subscriber."
),
SubscribeError::SendEmailError(_) => {
write!(f, "Failed to send a confirmation email.")
},
}
}
}
Debug很容易解决:我们为 SubscribeError 实现了 Error Trait,包括 source,我们可以再次使用我们之前为 StoreTokenError 写的辅助函数(error_chain_fmt)。
当涉及到 Display 时,我们有一个问题--同样的DatabaseError变体被用于在以下情况下遇到的错误:
- 从池中获取一个新的Postgres连接。
- 在subscribers表中插入一个订阅者。
- 提交SQL事务。
当为 SubscribeError 实现 Display 时,我们没有办法区分这三种情况中的哪一种,也就是说底层的错误类型是不够的。让我们通过为每个操作使用不同的枚举变量来消除歧义:
//! src/routes/subscriptions.rs
// [...]
pub enum SubscribeError {
// [...]
// No more `DatabaseError`
PoolError(sqlx::Error),
InsertSubscriberError(sqlx::Error),
TransactionCommitError(sqlx::Error),
}
impl std::error::Error for SubscribeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
// [...]
// No more DatabaseError
SubscribeError::PoolError(e) => Some(e),
SubscribeError::InsertSubscriberError(e) => Some(e),
SubscribeError::TransactionCommitError(e) => Some(e),
// [...]
}
}
}
impl std::fmt::Display for SubscribeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
// [...]
SubscribeError::PoolError(_) => {
write!(f, "Failed to acquire a Postgres connection from the pool")
}
SubscribeError::InsertSubscriberError(_) => {
write!(f, "Failed to insert new subscriber in the database.")
}
SubscribeError::TransactionCommitError(_) => {
write!(
f,
"Failed to commit SQL transaction to store a new subscriber."
)
}
}
}
}
impl ResponseError for SubscribeError {
fn status_code(&self) -> StatusCode {
match self {
SubscribeError::ValidationError(_) => StatusCode::BAD_REQUEST,
SubscribeError::PoolError(_)
| SubscribeError::TransactionCommitError(_)
| SubscribeError::InsertSubscriberError(_)
| SubscribeError::StoreTokenError(_)
| SubscribeError::SendEmailError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
DatabaseError 在另一个地方使用:
//! src/routes/subscriptions.rs
// [..]
impl From<sqlx::Error> for SubscribeError {
fn from(e: sqlx::Error) -> Self {
Self::DatabaseError(e)
}
}
仅仅是类型还不足以区分应该使用哪种枚举变量;我们不能为 sqlx::Error 实现 From。我们必须使用 map_err 来在每种情况下进行正确的转换。
//! src/routes/subscriptions.rs
// [..]
pub async fn subscribe(/* */) -> Result<HttpResponse, SubscribeError> {
// [...]
let mut transaction = pool.begin().await.map_err(SubscribeError::PoolError)?;
let subscriber_id = insert_subscriber(&mut transaction, &new_subscriber)
.await
.map_err(SubscribeError::InsertSubscriberError)?;
// [...]
transaction
.commit()
.await
.map_err(SubscribeError::TransactionCommitError)?;
// [...]
}
代码编译后,exception.message 再次显示有用的信息:
...
INFO: [HTTP REQUEST - END]
exception.details="Failed to store the confirmation token
for a new subscriber.
Caused by:
A database failure was encountered while trying to store
a subscription token.
Caused by:
error returned from database: column 'subscription_token'
of relation 'subscription_tokens' does not exist"
exception.message="Failed to store the confirmation token for a new subscriber.",
target=tracing_actix_web::root_span_builder,
http.status_code=500