「深挖Rust」错误处理 — 7

196 阅读2分钟

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


面向控制流

上面我们达到了我们想要的结果(获取有用的日志),但不太喜欢这个解决方案:我们的Web框架中为一个操作:store_token,此操作返回的错误类型实现了一个trait(ResponseError),而这个trait对REST或HTTP协议无感知。我们可以从一个不同的入口(例如CLI)调用 store_token。它的实现不应该有任何改变。

即使假设我们只是在REST API的上下文中调用 store_token,我们也可能添加其他依赖该函数的点 —— 它们可能不希望在失败时返回500。

当错误发生时,请求处理程序应当选择适当的HTTP状态代码返回,它不应该在其他地方泄露。

为了实现正确的关注点分离,我们需要引入另一种错误类型,即 SubscribeError。我们将使用它作为 subscribe() 的错误返回类型,它将拥有HTTP相关的逻辑(实现 ResponseError)。

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

pub async fn subscribe(/* */) -> Result<HttpResponse, SubscribeError> {
    // [...]
}

#[derive(Debug)]
struct SubscribeError {}

impl std::fmt::Display for SubscribeError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Failed to create a new subscriber."
        )
    }
}

impl std::error::Error for SubscriberError {}

impl ResponseError for SubscribeError {}

如果你运行 cargo check,你会看到雪崩式的 ? 无法将错误转换为 SubscribeError。我们需要由我们业务函数返回的错误和SubscribeError返回的错误类型之间的转换。

枚举错误

枚举是解决这个问题的最常见的方法:我们需要处理的每种错误类型提供一个变量。

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

#[derive(Debug)]
pub enum SubscribeError {
    ValidationError(String),
    DatabaseError(sqlx::Error),
    StoreTokenError(StoreTokenError),
    SendEmailError(reqwest::Error),
}

然后,我们可以在我们的处理程序中利用 ? 操作符,为每个包裹的错误类型提供 From 实现:

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

impl From<reqwest::Error> for SubscribeError {
    fn from(e: reqwest::Error) -> Self {
        Self::SendEmailError(e)
    }
}

impl From<sqlx::Error> for SubscribeError {
    fn from(e: sqlx::Error) -> Self {
        Self::DatabaseError(e)
    }
}

impl From<StoreTokenError> for SubscribeError {
    fn from(e: StoreTokenError) -> Self {
        Self::StoreTokenError(e)
    }
}

impl From<String> for SubscribeError {
    fn from(e: String) -> Self {
        Self::ValidationError(e)
    }
}

现在我们可以通过删除所有这些 match/if fallible_function().is_err() 来清理我们的请求处理程序:

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

pub async fn subscribe(/* */) -> Result<HttpResponse, SubscribeError> {
    let new_subscriber = form.0.try_into()?;
    let mut transaction = pool.begin().await?;
    let subscriber_id = insert_subscriber(/* */).await?;
    let subscription_token = generate_subscription_token();
    store_token(/* */).await?;
    transaction.commit().await?;
    send_confirmation_email(/* */).await?;
    Ok(HttpResponse::Ok().finish())
}

代码可以编译,但是我们的一个测试失败了:

thread 'subscriptions::subscribe_returns_a_400_when_fields_are_present_but_invalid'
panicked at 'assertion failed: `(left == right)`
  left: `400`,
 right: `500`: The API did not return a 400 Bad Request when the payload was empty name.'

我们仍然在使用 ResponseError 的默认实现 —— 它总是返回500。 这就是枚举的闪光点:我们可以使用控制流的匹配语句 → 根据我们所处理的故障情况,我们的行为是不同的。

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

impl ResponseError for SubscribeError {
    fn status_code(&self) -> StatusCode {
        match self {
            SubscribeError::ValidationError(_) => StatusCode::BAD_REQUEST,
            SubscribeError::DatabaseError(_)
            | SubscribeError::StoreTokenError(_)
            | SubscribeError::SendEmailError(_) => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }
}

测试应该再次通过。