* Copyright(c) 2024-2026 China Telecom Cloud Technologies Co., Ltd. All rights
* reserved. ctscat is licensed under Mulan PSL v2. You can use this software
* according to the terms and conditions of the Mulan PSL V2. You may obtain a
* copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2.
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. See the Mulan PSL v2 for
* more details.
*/
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ApiError {
#[error("HTTP 请求失败: {0}")]
HttpError(#[from] reqwest::Error),
#[error("认证失败: {0}")]
AuthenticationError(String),
#[error("API 限流: {0}")]
RateLimitError(String),
#[error("资源不存在: {0}")]
NotFoundError(String),
#[error("服务器错误: {0}")]
ServerError(String),
#[error("JSON 解析失败: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Base64 解码失败: {0}")]
Base64Error(String),
#[error("配置错误: {0}")]
InvalidConfig(String),
#[error("请求超时")]
TimeoutError,
#[error("未知错误: {0}")]
Unknown(String),
}
pub type ApiResult<T> = Result<T, ApiError>;
impl ApiError {
pub fn from_status(status: u16, message: String) -> Self {
match status {
401 | 403 => Self::AuthenticationError(message),
404 => Self::NotFoundError(message),
429 => Self::RateLimitError(message),
500..=599 => Self::ServerError(message),
_ => Self::Unknown(message),
}
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
Self::TimeoutError
| Self::ServerError(_)
| Self::RateLimitError(_)
| Self::HttpError(_)
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_status() {
let err = ApiError::from_status(401, "Unauthorized".to_string());
assert!(matches!(err, ApiError::AuthenticationError(_)));
let err = ApiError::from_status(403, "Forbidden".to_string());
assert!(matches!(err, ApiError::AuthenticationError(_)));
let err = ApiError::from_status(404, "Not Found".to_string());
assert!(matches!(err, ApiError::NotFoundError(_)));
let err = ApiError::from_status(429, "Too Many Requests".to_string());
assert!(matches!(err, ApiError::RateLimitError(_)));
let err = ApiError::from_status(500, "Internal Server Error".to_string());
assert!(matches!(err, ApiError::ServerError(_)));
let err = ApiError::from_status(503, "Service Unavailable".to_string());
assert!(matches!(err, ApiError::ServerError(_)));
let err = ApiError::from_status(400, "Bad Request".to_string());
assert!(matches!(err, ApiError::Unknown(_)));
}
#[test]
fn test_is_retryable() {
assert!(ApiError::TimeoutError.is_retryable());
assert!(ApiError::ServerError("error".to_string()).is_retryable());
assert!(ApiError::RateLimitError("error".to_string()).is_retryable());
assert!(!ApiError::AuthenticationError("error".to_string()).is_retryable());
assert!(!ApiError::NotFoundError("error".to_string()).is_retryable());
assert!(!ApiError::Base64Error("error".to_string()).is_retryable());
assert!(!ApiError::InvalidConfig("error".to_string()).is_retryable());
assert!(!ApiError::Unknown("error".to_string()).is_retryable());
}
#[test]
fn test_error_display() {
let err = ApiError::AuthenticationError("auth failed".to_string());
assert_eq!(format!("{}", err), "认证失败: auth failed");
let err = ApiError::NotFoundError("not found".to_string());
assert_eq!(format!("{}", err), "资源不存在: not found");
let err = ApiError::TimeoutError;
assert_eq!(format!("{}", err), "请求超时");
}
}