• 在Rust中处理错误的正确姿势
  • 发布于 1周前
  • 82 热度
    0 评论
  • Fayer
  • 0 粉丝 51 篇博客
  •   
当Rust项目规模超过12个crate时,由于Rust的强类型特性,match会处理所有边缘情况,因此组织和处理错误会变得相当困难。下面介绍一个用于在Rust项目中组织和处理错误的模式,到目前为止,它的可扩展性非常好!为了说明这种模式,我们将以一个单一的模块化web应用程序为例,该应用程序使用不同的服务来处理业务领域的不同“有界上下文”。

一个全局错误类型和多个本地错误类型
项目的核心错误类型是全局的,通常在项目根下的错误包中定义。
errs/error.rs
/// Error枚举表示面向用户的错误
#[derive(Debug, Clone, thiserror::Error)]
pub enum Error {
    #[error("{0}")]
    NotFound(String),
    #[error("{0}")]
    InvalidArgument(String),
    #[error("Internal error.")]
    Internal(String),
    #[error("{0}")]
    PermissionDenied(String),
    #[error("{0}")]
    AlreadyExists,
    #[error("Authentication required")]
    AuthenticationRequired,
}
这种错误类型应该足够通用,可以处理应用程序可能遇到的所有类型的错误。它用于在有界上下文(服务)之间传播错误。然后,每个服务定义自己的细粒度错误类型,并使用impl From<Error> for errs::Error定义特定于域的错误类型,以便本地错误可以使用"?"操作符。
services/users/error.rs
#[derive(Clone, Debug, thiserror::Error)]
pub enum Error {
    #[error("You must not be authenticated to perform this action")]
    MustNotBeAuthenticated,
    #[error("kernel: {0}")]
    Internal(String),
    #[error("Authentication required")]
    AuthenticationRequired,
    #[error("Password or email is not valid.")]
    PasswordOrEmailIsNotValid,
    #[error("User not found.")]
    UserNotFound,
    #[error("Session is not valid")]
    SessionIsNotValid,
    #[error("Permission denied")]
    PermissionDenied,

    // ...
}

impl From<Error> for errs::Error {
    fn from(value: Error) -> Self {
        match value {
            err @ Error::MustNotBeAuthenticated => errs::Error::PermissionDenied(err.to_string()),
            err @ Error::Internal(_) => errs::Error::Internal(err.to_string()),
            // ...
        }
    }
}
不同的服务不应该返回本地的错误类型。相反,它们必须返回全局errors::Error类型。本地错误不应该跨越上下文边界。
services/users/get_user.rs
impl UsersService {
    pub async fn get_user(
        &self,
        ctx: &RequestContext,
        input: GetUserInput,
    ) -> Result<User, errs::Error> {
        let current_user = ctx.user()?;

        // ...

        let user = self.repo.find_user(&self.db, input.id).await?;
        return Ok(user);
    }
}
这里,find_user返回类型为users:: Error的错误,如users:: error::Database或users:: error::UserNotFound,它不会在UsersService之外发生。而当使用“?”操作符时,转换为errors:: Error,如errors::Internal或errors::NotFound。因此,当Sales服务调用get_user时,后者不必知道如何处理Users绑定上下文的所有特性,而是可以直接将错误传播到表示层。
impl SalesService {
    pub async fn something(
        &self,
        ctx: &RequestContext,
        input: SomethingInput,
    ) -> Result<SomethingElse, errs::Error> {
        // ...
        // 堆代码 duidaima.com
        // 这里没有错误处理,我们可以直接传播错误
        let user = self.users.get_user(ctx, user_id).await?;

        // ...
    }
}
集中错误处理
这种全局和局部错误模式在表示层进行集中错误处理。对于HTTP服务,它可以像下面这样简单:
#[derive(Serialize)]
pub struct ApiError {
    pub message: String,
    pub code: String,
}

fn handle_error(err: errs::Error) -> HttpResponse {
    let (status, code) = match &err {
        Error::AuthenticationRequired => (StatusCode::UNAUTHORIZED, "UNAUTHORIZED"), // 401
        Error::PermissionDenied(_) => (StatusCode::FORBIDDEN, "FORBIDDEN"),          // 403
        Error::NotFound(_) => (StatusCode::NOT_FOUND, "NOT_FOUND"),                  // 404
        _ => (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_SERVER_ERROR"),           // 500
    };

    let message = match &self {
        err @ Error::Internal(_) => {
            error!("{err}");
            err.to_string()
        }
        err @ _ => err.to_string(),
    };

    let res = ApiError {
        message,
        code,
    };
    return (status, Json(res)).into_response();
}
它可以减少错误处理逻辑的复杂性,并防止致命的错误,例如将内部错误泄漏给最终用户。
用户评论