错误

errors.md
commit - 4d8d53cea59bca095ca5c02ef81f0b1791736855 - 2020.09.12

actix-web 使用它自带的 actix_web::error::Error 类型和 actix_web::error::ResponseError trait 来处理 web handler 的错误。

如果 handler 在实现了 ResponseError trait 的 Result 中返回 Error(具体指常规的 Rust trait std::error::Error),则 actix-web 会将该错误呈现为一个 HTTP 响应,并使用相应的状态码 actix_web::http::StatusCode。默认情况下,内部服务器会产生错误:

#![allow(unused)]
fn main() {
pub trait ResponseError {
    fn error_response(&self) -> Response<Body>;
    fn status_code(&self) -> StatusCode;
}
}

Responder trait 会将兼容的 Result 强制转换为 HTTP 响应:

#![allow(unused)]
fn main() {
impl<T: Responder, E: Into<Error>> Responder for Result<T, E>
}

上述源代码中的 Error 是 actix-web 的错误定义,任何实现了 ResponseError trait 的错误都可以被自动转换。

actix-web 为一些常见的 non-actix(非 actix 相关)错误提供了 ResponseError trait 实现。例如,如果 handlerio::Error 响应,则该错误将转换为 HttpInternalServerError

#![allow(unused)]
fn main() {
use std::io;
use actix_files::NamedFile;

fn index(_req: HttpRequest) -> io::Result<NamedFile> {
    Ok(NamedFile::open("static/index.html")?)
}
}

已经实现 ResponseError trait 的外部类型,其完整清单请参见 actix-web API 文档

自定义错误响应

下述代码是实现了 ResponseError trait 的示例,它使用 derive_more crate 来声明错误枚举。

use actix_web::{error, Result};
use derive_more::{Display, Error};

#[derive(Debug, Display, Error)]
#[display(fmt = "my error: {}", name)]
struct MyError {
    name: &'static str,
}

// Use default implementation for `error_response()` method
impl error::ResponseError for MyError {}

async fn index() -> Result<&'static str, MyError> {
    Err(MyError { name: "test" })
}

ResponseError 有一个默认的 error_response() 实现,它将渲染一个 HTTP-500 错误(内部服务器错误)。上文示例代码中,当我们执行 index handler 时,就会发生这种 HTTP-500 错误。

error_response() 方法可以重写,以生成更有用的结果:

use actix_web::{
    dev::HttpResponseBuilder, error, get, http::header, http::StatusCode, App, HttpResponse,
};
use derive_more::{Display, Error};

#[derive(Debug, Display, Error)]
enum MyError {
    #[display(fmt = "internal error")]
    InternalError,

    #[display(fmt = "bad request")]
    BadClientData,

    #[display(fmt = "timeout")]
    Timeout,
}

impl error::ResponseError for MyError {
    fn error_response(&self) -> HttpResponse {
        HttpResponseBuilder::new(self.status_code())
            .set_header(header::CONTENT_TYPE, "text/html; charset=utf-8")
            .body(self.to_string())
    }

    fn status_code(&self) -> StatusCode {
        match *self {
            MyError::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
            MyError::BadClientData => StatusCode::BAD_REQUEST,
            MyError::Timeout => StatusCode::GATEWAY_TIMEOUT,
        }
    }
}

#[get("/")]
async fn index() -> Result<&'static str, MyError> {
    Err(MyError::BadClientData)
}

错误助手

actix-web 提供了一系列错误助手(错误帮助程序)函数。在从其它错误生成特定的 HTTP 错误代码时,这些函数对于非常有用。下文的示例中,结构体 MyError 并未实现 ResponseError trait,我们使用 map_err 函数将其转换为 HTTP-400 错误(错误请求):

use actix_web::{error, get, App, HttpServer, Result};

#[derive(Debug)]
struct MyError {
    name: &'static str,
}

#[get("/")]
async fn index() -> Result<&'static str> {
    let result: Result<&'static str, MyError> = Err(MyError { name: "test error" });

    Ok(result.map_err(|e| error::ErrorBadRequest(e.name))?)
}

有关可用的错误帮助程序的完整列表,请参阅actix-web 中 error 模块的 API 文档

错误日志

WARN 日志级别,actix 记录所有错误。如果应用程序的日志级别设置为 DEBUG,并且启用了回溯功能 RUST_BACKTRACE,则回溯日志也会被记录。这些可通过环境变量进行配置:

>> RUST_BACKTRACE=1 RUST_LOG=actix_web=debug cargo run

可用情况下,Error 类型会使用具体请求的错误回溯。如果是底层失败而没有提供回溯,则会构造一个新的回溯,指向发生错误转换的位置(而不是错误的起源)。

错误处理的推荐方式

考虑将应用程序产生的错误分为两大类:面向用户的错误、不面向用户的错误。

面向用户的错误的一个例子:我们可以为可能发生的失败情形,指定一个 UserError 枚举,该枚举封装了 ValidationError,以便于在用户发送错误的输入时,返回验证信息:

use actix_web::{
    dev::HttpResponseBuilder, error, get, http::header, http::StatusCode, App, HttpResponse,
    HttpServer,
};
use derive_more::{Display, Error};

#[derive(Debug, Display, Error)]
enum UserError {
    #[display(fmt = "Validation error on field: {}", field)]
    ValidationError { field: String },
}

impl error::ResponseError for UserError {
    fn error_response(&self) -> HttpResponse {
        HttpResponseBuilder::new(self.status_code())
            .set_header(header::CONTENT_TYPE, "text/html; charset=utf-8")
            .body(self.to_string())
    }
    fn status_code(&self) -> StatusCode {
        match *self {
            UserError::ValidationError { .. } => StatusCode::BAD_REQUEST,
        }
    }
}

这将完全按照预期的方式运行,因为使用 display 定义的错误消息,其编写的明确意图即是为了用户读取。

然而,并不是所有错误都需要发回错误消息——在服务器环境中会发生许多错误,我们可能希望对用户隐藏细节。例如,如果数据库关闭,客户端会产生连接超时错误;或者 HTML 模板格式不正确,并且在呈现时出错。在这些情况下,最好将错误映射为适合用户处理的通用错误。

下面的示例代码中,使用自定义消息将内部错误映射为面向用户的 InternalError

use actix_web::{
    dev::HttpResponseBuilder, error, get, http::header, http::StatusCode, App, HttpResponse,
    HttpServer,
};
use derive_more::{Display, Error};

#[derive(Debug, Display, Error)]
enum UserError {
    #[display(fmt = "An internal error occurred. Please try again later.")]
    InternalError,
}

impl error::ResponseError for UserError {
    fn error_response(&self) -> HttpResponse {
        HttpResponseBuilder::new(self.status_code())
            .set_header(header::CONTENT_TYPE, "text/html; charset=utf-8")
            .body(self.to_string())
    }
    fn status_code(&self) -> StatusCode {
        match *self {
            UserError::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }
}

#[get("/")]
async fn index() -> Result<&'static str, UserError> {
    do_thing_that_fails().map_err(|_e| UserError::InternalError)?;
    Ok("success!")
}

通过将错误分为面向用户的错误和不面向用户的错误,我们可以确保不会意外地将应用程序内部错误暴露给用户——这些错误是用户不希望看到的。

记录错误

使用日志中间件 middleware::Logger 记录错误日志的示例:

use actix_web::{error, get, middleware::Logger, App, HttpServer, Result};
use log::debug;
use derive_more::{Display, Error};

#[derive(Debug, Display, Error)]
#[display(fmt = "my error: {}", name)]
pub struct MyError {
    name: &'static str,
}

// Use default implementation for `error_response()` method
impl error::ResponseError for MyError {}

#[get("/")]
async fn index() -> Result<&'static str, MyError> {
    let err = MyError { name: "test error" };
    debug!("{}", err);
    Err(err)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    std::env::set_var("RUST_LOG", "my_errors=debug,actix_web=info");
    std::env::set_var("RUST_BACKTRACE", "1");
    env_logger::init();

    HttpServer::new(|| App::new().wrap(Logger::default()).service(index))
        .bind("127.0.0.1:8080")?
        .run()
        .await
}