错误处理

Rust 应用中的常规错误处理方式

Rust 的错误处理不同于 Java 等语言, 它没有 try...catch 这种玩意, 正常的做法是在应用程序层面定义全局的错误处理类型:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("io: `{0}`")]
    Io(#[from] io::Error),
    #[error("utf8: `{0}`")]
    FromUtf8(#[from] FromUtf8Error),
    #[error("diesel: `{0}`")]
    Diesel(#[from] diesel::result::Error),
    ...
}

pub type AppResult<T> = Result<T, AppError>;

这里使用了 thiserror 这个库, 它可以方便地定义你自己的自定义错误类型, 简化代码. 为了简单书写, 顺便定义一个 AppResult.

thiserror 与 anyhow

在 Rust 错误处理的生态中,两个常用的库是 thiserroranyhow

  • thiserror: 适用于库开发者,用于定义清晰的错误类型。它通过派生宏帮助您为自定义错误类型实现 std::error::Error trait,同时允许您定义错误的表示方式。当您构建一个库或者需要为用户提供清晰的错误类型时,thiserror 是更好的选择。

  • anyhow: 适用于应用开发者,提供了一个通用的错误类型 anyhow::Error,能够包含任何实现了 std::error::Error trait 的错误。它更侧重于错误的传播而非定义,尤其适合应用层代码,您可以快速将各种错误转换为 anyhow::Error,减少编写样板代码的需要。

在某些场景中,您可能会同时使用这两个库:在库中使用 thiserror 定义错误类型,在应用中使用 anyhow 处理和传播这些错误。

Handler 中的错误处理

在 Salvo 中, Handler 也经常会遇到各式错误, 比如: 数据库连接错误, 文件访问错误, 网络连接错误等等. 对于这个类型的错误, 可以采用上述的错误处理手法:

#[handler]
async fn home()-> AppResult<()> {

}

这里的 home 就直接返回了一个 AppResult<()>. 但是, 这个错误该如何显示呢? 我们需要为 AppResult 这个自定义错误类型实现 Writer, 在这个实现中我们可以决定如何显示错误:

#[async_trait]
impl Writer for AppError {
    async fn write(mut self, _req: &mut Request, depot: &mut Depot, res: &mut Response) {
        res.render(Text::Plain("I'm a error, hahaha!"));
    }
}

Salvo 中的 Handler 可以返回 Result,只需要 Result 中的 OkErr 的类型都实现 Writer trait。

使用 anyhow 进行错误处理

考虑到 anyhow 的使用比较广泛,Salvo 提供了对 anyhow::Error 的内置支持。在开启 anyhow 功能后,anyhow::Error 会实现 Writer trait,它会被映射为 InternalServerError

#[cfg(feature = "anyhow")]
#[async_trait]
impl Writer for ::anyhow::Error {
    async fn write(mut self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
        res.render(StatusError::internal_server_error());
    }
}

要使用 anyhow 功能,需要在 Cargo.toml 中启用 Salvo 的 anyhow feature:

[dependencies]
salvo = { version = "*", features = ["anyhow"] }
anyhow = "1.0"

这样,您的处理函数就可以直接返回 anyhow::Result<T>

#[handler]
async fn home() -> anyhow::Result<impl Writer> {
    let data = fetch_data().context("Failed to fetch data")?;
    Ok(Text::Plain(data))
}

Error 中往往包含一些敏感信息, 一般情况下, 并不想被普通用户看到, 那样也太不安全了, 一点点隐私也没有了. 但是, 如果你是开发人员或者网站管理员, 或许想法就不一样了, 你希望错误能把外衣脱得光光的, 让你看到最真实的错误信息.

可以看到, write 的方法中, 我们其实是可以拿到 RequestDepot 的引用的, 这就可以很方便地实现上面的骚操作了:

#[async_trait]
impl Writer for AppError {
    async fn write(mut self, _req: &mut Request, depot: &mut Depot, res: &mut Response) {
        let user = depot.obtain::<User>();
        if user.is_admin {
            res.render(Text::Plain(e.to_string()));
        } else {
            res.render(Text::Plain("I'm a error, hahaha!"));
        }
    }
}

错误页面的显示

Salvo 中自带的错误页面在绝大部分情况下是满足需求的, 它可以根据请求的数据类型显示 Html, Json 或者 Xml 页面. 然而, 某些情况下, 我们依然期望自定义错误页面的显示.

这个可以通过自定义 Catcher 实现. 详细的介绍可以查看 Catcher 部分的讲解.