Panics

...但是我们的测试结果并不理想:

running 4 tests
test subscribe_returns_a_200_when_fields_are_present_but_empty ... FAILED
test subscribe_returns_a_200_for_valid_form_data ... ok
test subscribe_returns_a_400_when_data_is_missing ... ok
test health_check_works ... ok

failures:

---- subscribe_returns_a_200_when_fields_are_present_but_empty stdout ----

thread 'actix-server worker 0' panicked at src/domain.rs:33:13:
 is not a valid subscriber name
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

thread 'subscribe_returns_a_200_when_fields_are_present_but_empty' panicked at t
ests/health_check.rs:179:14:
Failed to execute request.: reqwest::Error { kind: Request, url: "http://127.0.0
.1:42035/subscriptions", source: hyper_util::client::legacy::Error(SendRequest, 
hyper::Error(IncompleteMessage)) }

好的一面是:我们不再为空名称返回 200 OK 错误。

不好的一面是: 我们的 API 会突然终止请求处理,导致客户端 观察到 IncompleteMessage 错误。这不太优雅。

让我们修改测试以反映我们的新期望:当有效负载包含无效数据时,我们希望看到 400 Bad Request 响应。

//! tests/health_check.rs
// [...]

async fn subscribe_returns_a_200_when_fields_are_present_but_invalid() {
    // Arrange
    let app = spawn_app().await;
    let client = reqwest::Client::new();
    let test_cases = vec![
        ("name=&email=ursula_le_guin%40gmail.com", "empty name"),
        ("name=Ursula&email=", "empty email"),
        ("name=Ursula&email=definitely-not-an-email", "invalid email"),
    ];
    for (body, description) in test_cases {
        // Act
        let response = client
            .post(&format!("{}/subscriptions", &app.address))
            .header("Content-Type", "application/x-www-form-urlencoded")
            .body(body)
            .send()
            .await
            .expect("Failed to execute request.");

        // Assert
        assert_eq!(
            // Not 200 anymore!
            400,
            response.status().as_u16(),
            "The API did not return a 400 OK when the payload was {}.",
            description
        );
    }
}

现在,让我们看看根本原因——当 SubscriberName::parse 中的验证检查失败时,我们选择 panic:

//! src/domain.rs
// [...]
pub fn parse(s: String) -> SubscriberName {
    // [...]

    if is_empty_or_whitespace || is_too_long || contains_forbidden_characters {
        panic!("{s} is not a valid subscriber name");
    } else {
        Self(s)
    }
}

Rust 中的 panic 用于处理不可恢复的错误: 意料之外的故障模式,或者我们无法有效恢复的故障模式。例如,主机内存不足或磁盘已满。Rust 的 panic 并不等同于 Python、C# 或 Java 等语言中的异常。虽然 Rust 提供了一些工具来捕获(某些) panic, 但这绝对不是推荐的方法,应该谨慎使用。Burntsushi 几年前在 Reddit 的一个帖子中就曾明确指出:

[...] 如果您的 Rust 应用程序在响应任何用户输入时出现 panic,则以下情况应该为真:您的应用程序存在错误,无论该错误存在于库中还是主应用程序代码中。

从这个角度来看,我们可以理解正在发生的事情: 当我们的请求处理程序发生恐慌时,actix-web 会认为发生了可怕的事情,并立即丢弃正在处理该 panic 请求的工作进程。

如果恐慌不是解决问题的办法,我们应该用什么来处理可恢复的错误?