报错expected enum Result<_, InfraError> found enum Result<_, anyhow::Error>
对于以下代码
解决方案
1、由于这里使用thiserror自定义了InfraError,而anyhow默认有一个anyhow::Ok;的方法,使用两个库结合时应当返回标准库中的Ok(),删除anyhow的Ok即可
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去匹配错误,又丢失了底层错误细节,所以最好选一个一把梭