报错expected enum`Result<_, InfraError>` found enum `Result<_, anyhow::Error>`:已解决

200 阅读9分钟

报错expected enum Result<_, InfraError> found enum Result<_, anyhow::Error>

对于以下代码

image.png

解决方案

1、由于这里使用thiserror自定义了InfraError,而anyhow默认有一个anyhow::Ok;的方法,使用两个库结合时应当返回标准库中的Ok(),删除anyhowOk即可

image.png

2、统一使用thiserror自定义错误

thiserror实现了标准库的std::error::Error

优点:

  • 适合编写库crate
  • 适合需要明确错误的应用
  • 明确错误类型

缺点:

  • 定义复杂
  • 传播复杂(可能需要match或map_err传播)
  • 可读性差

thiserror各层独自处理错误

如以下自定义错误

use bb8_redis::redis;
use thiserror::Error;
// 自定义错误类型
#[derive(Error, Debug)]
pub enum DomainError {
    // 1. 实体相关错误
    #[error("User entity validation error: {0}")]
    UserEntityValidationError(String),
    #[error("User not found with ID: {0}")]
    UserNotFoundError(String),
    #[error("User already exists with email: {0}")]
    UserAlreadyExistsError(String),
    #[error("Invalid user role: {0}")]
    InvalidUserRoleError(String),
    // 2. 业务规则相关错误
    #[error("Insufficient balance for user: {0}")]
    InsufficientBalanceError(u32),
    #[error("User is not eligible for this operation")]
    UserNotEligibleError,
    #[error("Operation not allowed at this time for user: {0}")]
    OperationNotAllowedError(u32),
    // 3. 仓储层相关错误(如果仓储层的错误需要在领域层进行特殊处理)
    #[error("Database error while saving user: {0}")]
    DatabaseSaveUserError(String),
    #[error("Database error while retrieving user: {0}")]
    DatabaseRetrieveUserError(String),
    #[error("Database connection error: {0}")]
    DatabaseConnectionError(String),
    // 4. 领域服务相关错误
    #[error("Password verification failed for user: {0}")]
    PasswordVerificationError(u32),
    #[error("Token generation failed for user: {0}")]
    TokenGenerationError(u32),
    #[error("Token verification failed")]
    TokenVerificationError,
    // 5. 与领域内数据一致性相关的错误
    #[error("Data integrity violation in user profile")]
    UserProfileDataIntegrityError,
    #[error("Inconsistent user state: {0}")]
    InconsistentUserStateException(String),
}
#[derive(Error, Debug)]
pub enum AppError {
    // {0}是格式化占位符,使用时将其替换为实际的错误消息。
    #[error("Request parameter error: {0}")]
    ReqParamError(String),
    #[error("Delete error: {0}")]
    ReqDeleteFail(String),
    #[error("IO error: {0}")]
    IOError(String),
    #[error("Register error: {0}")]
    RegisterError(String),
    #[error("Login error: {0}")]
    LoginError(String),
    #[error("Authenticate error: {0}")]
    AuthenticateError(String),
    #[error("Refresh token error: {0}")]
    RefreshTokenError(String),
    #[error("Network error: {0}")]
    NetworkError(String),
    #[error("Other error: {0}")]
    OtherError(String),
}
// 基础设施层错误类型
#[derive(Error, Debug)]
pub enum InfraError {
    #[error("Convert error: {0}")]
    ConvertError(String),
    #[error("Database error: {0}")]
    DatabaseError(#[from] sea_orm::DbErr),
    #[error(transparent)]
    RedisError(#[from] redis::RedisError),
    #[error("IO error: {0}")]
    IoError(#[from] std::io::Error),
    #[error("Connection error: {0}")]
    ConnectionError(String),
    #[error("User not found")]
    UserNotFound,
    #[error("User create error: {0}")]
    UserError(String),
    #[error("Insert error: {0}")]
    InsertError(String),
    #[error("Message error: {0}")]
    MessageError(String),
    #[error("Sending error: {0}")]
    SendingError(String),
    #[error("Password hash error: {0}")]
    PasswordHashError(String),
    #[error("Password verify error: {0}")]
    PasswordVerifyError(String),
    #[error("Jwt encode error: {0}")]
    JwtEncodeError(String),
    #[error("Jwt decode error: {0}")]
    JwtDecodeError(String),
    #[error("Network timeout error: {0}")]
    NetworkTimeoutError(String),
    #[error("Network connection error: {0}")]
    NetworkConnectionError(String),
    #[error("Config load error: {0}")]
    ConfigLoadError(String),
    #[error("Invalid input format error: {0}")]
    InvalidInputFormatError(String),
    #[error("Missing required field error: {0}")]
    MissingRequiredFieldError(String),
    #[error("ES error: {0}")]
    TransportError(elasticsearch::Error),
    #[error("ES Client error: {0}")]
    ClientError(elasticsearch::Error),
    #[error("Other error: {0}")]
    OtherError(String),
}

使用

pub trait CustomerRepository{
    async fn find_all(&self) -> Result<Vec<Customer>, InfraError>;
    async fn find_by_email(&self, email: &str) -> Result<Option<Customer>, InfraError>;
    async fn save(&self, customer: Customer) -> Result<(), InfraError>;
    async fn find_by_id(&self, id: CustomerId) -> Result<Option<Customer>, InfraError>;
    async fn send_email(&self, email: &str) -> Result<(), InfraError>;
    async fn find_code_by_email(&self, email: &str) -> Result<Option<String>, InfraError>;
}

可以使用map_err将其他错误映射为自定义错误

    async fn find_code_by_email(&self, email: &str) -> Result<Option<String>, InfraError> {
        println!("email:{}", email);
        self.redis.get(email).await.map_err(|_| InfraError::OtherError("验证码已过期".to_string()))
    }

thiserror统一处理错误和响应

注意以下的类型别名,这种方式将对外的响应与错误细节结合

// 类型别名
pub type AppResult<T = ()> = std::result::Result<T, AppError>;

#[derive(Debug, thiserror::Error)]
pub enum AppError {
  #[error("{0} not found")]
  NotFoundError(Resource),
  #[error("{0} not available")]
  NotAvailableError(Resource),
  #[error("{0} already exists")]
  ResourceExistsError(Resource),
  #[error("{0}")]
  PermissionDeniedError(String),
  #[error("{0}")]
  UserNotActiveError(String),
  #[error("{0}")]
  InvalidSessionError(String),
  #[error("{0}")]
  ConflictError(String),
  #[error("{0}")]
  UnauthorizedError(String),
  #[error("bad request {0}")]
  BadRequestError(String),
  #[error("{0}")]
  InvalidPayloadError(String),
  #[error("{0}")]
  HashError(String),
  #[error(transparent)]
  InvalidInputError(#[from] garde::Report),
  #[error(transparent)]
  DatabaseError(#[from] sea_orm::error::DbErr),
  #[error(transparent)]
  WebSocketError(#[from] tokio_tungstenite::tungstenite::Error),
  #[error(transparent)]
  IoError(#[from] std::io::Error),
  #[error(transparent)]
  UuidError(#[from] uuid::Error),
  #[error(transparent)]
  JwtError(#[from] jsonwebtoken::errors::Error),
  #[error(transparent)]
  HttpClientError(#[from] reqwest::Error),
  #[error(transparent)]
  RedisError(#[from] redis::RedisError),
  #[error(transparent)]
  ConfigError(#[from] config::ConfigError),
  #[error(transparent)]
  SmtpError(#[from] lettre::transport::smtp::Error),
  #[error(transparent)]
  LetterError(#[from] lettre::error::Error),
  #[error(transparent)]
  ParseJsonError(#[from] serde_json::Error),
  #[error(transparent)]
  ParseFloatError(#[from] std::num::ParseFloatError),
  #[error(transparent)]
  AddrParseError(#[from] std::net::AddrParseError),
  #[error(transparent)]
  SpawnTaskError(#[from] tokio::task::JoinError),
  #[error(transparent)]
  TeraError(#[from] tera::Error),
  #[error(transparent)]
  Base64Error(#[from] base64::DecodeError),
  #[error(transparent)]
  StrumParseError(#[from] strum::ParseError),
  #[error(transparent)]
  SystemTimeError(#[from] std::time::SystemTimeError),
  #[error(transparent)]
  AxumError(#[from] axum::Error),
  #[error(transparent)]
  UnknownError(#[from] anyhow::Error),
  #[error(transparent)]
  Infallible(#[from] std::convert::Infallible),
  #[error(transparent)]
  TypeHeaderError(#[from] axum_extra::typed_header::TypedHeaderRejection),
}

impl From<argon2::password_hash::Error> for AppError {
  fn from(value: argon2::password_hash::Error) -> Self {
    AppError::HashError(value.to_string())
  }
}

impl AppError {
  pub fn response(self) -> (StatusCode, AppResponseError) {
    use AppError::*;
    let message = self.to_string();
    let (kind, code, details, status_code) = match self {
      InvalidPayloadError(_err) => (
        "INVALID_PAYLOAD_ERROR".to_string(),
        None,
        vec![],
        StatusCode::BAD_REQUEST,
      ),
      BadRequestError(_err) => (
        "BAD_REQUEST_ERROR".to_string(),
        None,
        vec![],
        StatusCode::BAD_REQUEST,
      ),
      NotAvailableError(resource) => (
        format!("{resource}_NOT_AVAILABLE_ERROR"),
        None,
        vec![],
        StatusCode::NOT_FOUND,
      ),
      NotFoundError(resource) => (
        format!("{resource}_NOT_FOUND_ERROR"),
        Some(resource.resource_type as i32),
        resource.details.clone(),
        StatusCode::NOT_FOUND,
      ),
      ResourceExistsError(resource) => (
        format!("{resource}_ALREADY_EXISTS_ERROR"),
        Some(resource.resource_type as i32),
        resource.details.clone(),
        StatusCode::CONFLICT,
      ),
      AxumError(_err) => (
        "AXUM_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      ConfigError(_err) => (
        "CONFIG_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      AddrParseError(_err) => (
        "ADDR_PARSE_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      IoError(err) => {
        let (status, kind, code) = match err.kind() {
          std::io::ErrorKind::NotFound => (
            StatusCode::NOT_FOUND,
            format!("{}_NOT_FOUND_ERROR", ResourceType::File),
            Some(ResourceType::File as i32),
          ),
          std::io::ErrorKind::PermissionDenied => {
            (StatusCode::FORBIDDEN, "FORBIDDEN_ERROR".to_string(), None)
          }
          _ => (
            StatusCode::INTERNAL_SERVER_ERROR,
            "IO_ERROR".to_string(),
            None,
          ),
        };
        (kind, code, vec![], status)
      }
      WebSocketError(_err) => (
        "WEBSOCKET_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      ParseJsonError(_err) => (
        "PARSE_JSON_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      StrumParseError(_err) => (
        "STRUM_PARSE_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      HttpClientError(_err) => (
        "HTTP_CLIENT_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      SystemTimeError(_err) => (
        "SYSTEM_TIME_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      SpawnTaskError(_err) => (
        "SPAWN_TASK_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      UnknownError(_err) => (
        "UNKNOWN_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      PermissionDeniedError(_err) => (
        "PERMISSION_DENIED_ERROR".to_string(),
        None,
        vec![],
        StatusCode::FORBIDDEN,
      ),
      InvalidSessionError(_err) => (
        "INVALID_SESSION_ERROR".to_string(),
        None,
        vec![],
        StatusCode::BAD_REQUEST,
      ),
      ConflictError(_err) => (
        "CONFLICT_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      UserNotActiveError(_err) => (
        "USER_NOT_ACTIVE_ERROR".to_string(),
        None,
        vec![],
        StatusCode::FORBIDDEN,
      ),
      UnauthorizedError(_err) => (
        "UNAUTHORIZED_ERROR".to_string(),
        None,
        vec![],
        StatusCode::UNAUTHORIZED,
      ),
      UuidError(_err) => (
        "UUID_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      JwtError(_err) => (
        "UNAUTHORIZED_ERROR".to_string(),
        None,
        vec![],
        StatusCode::UNAUTHORIZED,
      ),
      RedisError(_err) => (
        "REDIS_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      SmtpError(_err) => (
        "SMTP_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      LetterError(_err) => (
        "LETTER_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      HashError(_err) => (
        "HASH_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      ParseFloatError(_err) => (
        "PARSE_FLOAT_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      TeraError(_err) => (
        "TERA_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      Base64Error(_err) => (
        "BASE64_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      InvalidInputError(err) => (
        "INVALID_INPUT_ERROR".to_string(),
        None,
        err
          .iter()
          .map(|(p, e)| (p.to_string(), e.to_string()))
          .collect(),
        StatusCode::BAD_REQUEST,
      ),
      DatabaseError(_err) => (
        "DATABASE_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      Infallible(_err) => (
        "INFALLIBLE".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
      TypeHeaderError(_err) => (
        "TYPE_HEADER_ERROR".to_string(),
        None,
        vec![],
        StatusCode::INTERNAL_SERVER_ERROR,
      ),
    };

    (
      status_code,
      AppResponseError::new(kind, message, code, details),
    )
  }
}

impl IntoResponse for AppError {
  fn into_response(self) -> Response {
    let (status_code, body) = self.response();
    (status_code, Json(body)).into_response()
  }
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct AppResponseError {
  pub kind: String,
  pub error_message: String,
  pub code: Option<i32>,
  pub details: Vec<(String, String)>,
}

impl AppResponseError {
  pub fn new(
    kind: impl Into<String>,
    message: impl Into<String>,
    code: Option<i32>,
    details: Vec<(String, String)>,
  ) -> Self {
    Self {
    // 指定错误类型
      kind: kind.into(),
      error_message: message.into(),
      code,
      details,
    }
  }
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Resource {
  pub details: Vec<(String, String)>,
  pub resource_type: ResourceType,
}

impl std::fmt::Display for Resource {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    // TODO
    self.resource_type.fmt(f)
  }
}

#[derive(Debug, EnumString, strum::Display, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ResourceType {
  #[strum(serialize = "USER")]
  User,
  #[strum(serialize = "FILE")]
  File,
  #[strum(serialize = "SESSION")]
  Session,
  #[strum(serialize = "MESSAGE")]
  Message,
}

pub fn invalid_input_error(field: &'static str, message: &'static str) -> AppError {
  let mut report = garde::Report::new();
  report.append(garde::Path::new(field), garde::Error::new(message));
  AppError::InvalidInputError(report)
}

使用

pub async fn check(redis: &RedisClient, claims: &UserClaims) -> AppResult<Uuid> {
  let session_key = SessionKey {
    user_id: claims.uid,
  };
  let session_id = crate::service::redis::get(redis, &session_key)
    .await?
    .ok_or_else(|| {
    // 这里指定错误类型
      AppError::NotFoundError(crate::error::Resource {
        details: vec![("session_key".to_string(), claims.sid.to_string())],
        resource_type: crate::error::ResourceType::Session,
      })
    })?;
  if claims.sid != session_id {
    info!("Session id invalid so deleting it: {session_key:?}.");
    crate::service::redis::del(redis, &session_key).await?;
    // 这里指定错误类型
    return Err(AppError::InvalidSessionError(
      "Session is Invalid".to_string(),
    ));
  }
  Ok(claims.uid)
}

?传播包含错误的响应

pub async fn info(
  state: &AppState,
  user: UserClaims,
  req: TokenInfoRequest,
) -> AppResult<UserClaims> {
  info!("Get token info by user_id: {}", user.uid);
  if user.rol != RoleUser::System {
    return Err(AppError::PermissionDeniedError(
      "This user does not have permission to use this resource.".to_string(),
    ));
  }
  let token_data = UserClaims::decode(&req.token, &ACCESS_TOKEN_DECODE_KEY)?;
  // 这里传播了上一段代码的错误信息
  service::session::check(&state.redis, &token_data.claims).await?;
  Ok(token_data.claims)
}

3、统一使用anyhow一把梭

Axum官方例子中使用的anyhow

优点:

  • 适合一般Web应用
  • 适合不关注具体错误的应用(可以通过错误信息细化具体错误)
  • 快速原型开发

缺点:

  • 丢失错误类型(需要根据不同错误类型进行不同重试策略的系统中不适合使用)

anyhow处理错误

对于这个退出登录的方法使用use anyhow::{Context, Result};

async fn logout(
    State(store): State<MemoryStore>,
    TypedHeader(cookies): TypedHeader<headers::Cookie>,
) -> Result<impl IntoResponse, AppError> {
    let cookie = cookies
        .get(COOKIE_NAME)
        .context("unexpected error getting cookie name")?;

    let session = match store
        .load_session(cookie.to_string())
        .await
        .context("failed to load session")?
    {
        Some(s) => s,
        // No session active, just redirect
        None => return Ok(Redirect::to("/")),
    };

    store
        .destroy_session(session)
        .await
        .context("failed to destroy session")?;

    Ok(Redirect::to("/"))
}

使用anyhow::Result替换标准库的Result,使用Context包装上下文,如包装错误信息进行精细化定位错误位置

对于错误信息可以直接用anyhow!()包装起来

return Err(anyhow!("Missing attribute: {}", missing));

anyhow统一错误和响应

使用anyhow按以上方式进行错误定义,传递错误后,外部不能直接响应错误信息,错误码的处理可以通过实现公共响应,其他内部错误处理直接用anyhow::Context包起来就可以了

#[derive(Debug, Serialize, Default)]
pub struct Res<T> {
    pub code: u16,
    pub data: Option<T>,
    pub msg: Option<String>,
}

impl<T> IntoResponse for Res<T> where T: Serialize + Send + Sync + Debug + 'static {
    fn into_response(self) -> Response {
        // 序列化响应体,如果序列化失败,返回默认的响应体
        let json_string = match serde_json::to_string(&self) {
            Ok(json) => json,
            Err(e) => {
                eprintln!("Failed to serialize response: {}", e);
                serde_json::json!({
                    "code": 500,
                    "data": null,
                    "msg": "Internal Server Error"
                }).to_string()
            }
        };
        // 添加响应头
        Response::builder()
            .status(StatusCode::from_u16(self.code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR))
            .header(header::CONTENT_TYPE, HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()))
            .body(Body::from(json_string))
            .unwrap()
    }
}

impl<T: Serialize> Res<T> {
    // 成功数据
    pub fn with_data(data: T) -> Self {
        Self {
            code: StatusCode::OK.as_u16(),
            data: Some(data),
            msg: Some("Success".to_string()),
        }
    }
    // 成功无数据
    pub fn with_success() -> Self {
        Self {
            code: StatusCode::OK.as_u16(),
            data: None,
            msg: Some("Success".to_string()),
        }
    }
    // 成功消息
    pub fn with_msg(msg: &str) -> Self {
        Self {
            code: StatusCode::OK.as_u16(),
            data: None,
            msg: Some(msg.to_string()),
        }
    }
    // 成功数据和消息
    #[allow(dead_code)]
    pub fn with_data_msg(data: T, msg: &str) -> Self {
        Self {
            code: StatusCode::OK.as_u16(),
            data: Some(data),
            msg: Some(msg.to_string()),
        }
    }
    // 失败消息
    pub fn with_err(err:&str) -> Self {
        Self {
            code: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
            data: None,
            msg: Some(err.to_string()),
        }
    }
}

使用响应

}
pub async fn send_email(
    State(app_state): State<AppState>,
    verify_code_send_dto: Json<VerifyCodeSendDto>
) -> impl IntoResponse {
    info!("verify: {:?}", verify_code_send_dto);
    let customer_repository_impl = CustomerRepositoryImpl::new(app_state.db);
    let use_case = CustomerUseCase::new(customer_repository_impl);
    match use_case.send_email(verify_code_send_dto.receive_email.clone()).await {
        Ok(()) => Res::<String>::with_success(),
        Err(err) => Res::with_err(&err.to_string()),
    }
}

总结

  • 开发简单Web应用适合使用anyhow,复杂Web应用适合thiserror
  • 开发库crate适合thiserror
  • 混合使用容易造成混乱(可以相信自己但不能相信猪队友)

两者结合使用容易导致应用复杂的例子,例如基础设施层返回InfraError,领域层转换为DomainError要做一层转换,到应用层还要转换一次;如果使用anyhow将错误传递到接口层再使用thiserror去匹配错误,又丢失了底层错误细节,所以最好选一个一把梭