use std::fmt;
#[derive(Debug)]
pub enum HttpError {
ConnectTimeout,
RequestTimeout,
ConnectionFailed(String),
ServerError(u16),
ClientError(u16, String),
Cancelled,
TooManyRedirects,
InvalidUrl(String),
InvalidHeaderName(String),
InvalidHeaderValue(String),
JsonSerialize(String),
JsonDeserialize(String),
FormEncode(String),
BodyTooLarge,
ResponseBody(String),
Request(reqwest::Error),
ClientBuild(String),
Other(String),
}
impl fmt::Display for HttpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ConnectTimeout => write!(f, "连接超时"),
Self::RequestTimeout => write!(f, "请求超时"),
Self::ConnectionFailed(msg) => write!(f, "连接失败: {}", msg),
Self::ServerError(code) => write!(f, "服务器错误: {}", code),
Self::ClientError(code, msg) => write!(f, "客户端错误 {}: {}", code, msg),
Self::Cancelled => write!(f, "请求被取消"),
Self::TooManyRedirects => write!(f, "重定向过多"),
Self::InvalidUrl(url) => write!(f, "无效的 URL: {}", url),
Self::InvalidHeaderName(name) => write!(f, "无效的请求头名称: {}", name),
Self::InvalidHeaderValue(value) => write!(f, "无效的请求头值: {}", value),
Self::JsonSerialize(msg) => write!(f, "JSON 序列化失败: {}", msg),
Self::JsonDeserialize(msg) => write!(f, "JSON 反序列化失败: {}", msg),
Self::FormEncode(msg) => write!(f, "表单编码失败: {}", msg),
Self::BodyTooLarge => write!(f, "请求体过大"),
Self::ResponseBody(msg) => write!(f, "响应体读取错误: {}", msg),
Self::Request(e) => write!(f, "请求错误: {}", e),
Self::ClientBuild(msg) => write!(f, "构建客户端失败: {}", msg),
Self::Other(msg) => write!(f, "{}", msg),
}
}
}
impl std::error::Error for HttpError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Request(e) => Some(e),
_ => None,
}
}
}
impl From<reqwest::Error> for HttpError {
fn from(err: reqwest::Error) -> Self {
if err.is_timeout() {
if err.is_connect() {
Self::ConnectTimeout
} else {
Self::RequestTimeout
}
} else if err.is_connect() {
Self::ConnectionFailed(err.to_string())
} else if err.is_redirect() {
Self::TooManyRedirects
} else {
Self::Request(err)
}
}
}
impl HttpError {
pub fn is_retryable(&self) -> bool {
matches!(
self,
Self::ConnectTimeout
| Self::RequestTimeout
| Self::ConnectionFailed(_)
| Self::ServerError(_)
)
}
pub fn is_client_error(&self) -> bool {
matches!(self, Self::ClientError(_, _))
}
pub fn is_server_error(&self) -> bool {
matches!(self, Self::ServerError(_))
}
pub fn status_code(&self) -> Option<u16> {
match self {
Self::ServerError(code) | Self::ClientError(code, _) => Some(*code),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_retryable_errors() {
assert!(HttpError::ConnectTimeout.is_retryable());
assert!(HttpError::RequestTimeout.is_retryable());
assert!(HttpError::ConnectionFailed("test".into()).is_retryable());
assert!(HttpError::ServerError(500).is_retryable());
assert!(HttpError::ServerError(503).is_retryable());
assert!(!HttpError::ClientError(400, "Bad Request".into()).is_retryable());
assert!(!HttpError::InvalidUrl("bad".into()).is_retryable());
assert!(!HttpError::Cancelled.is_retryable());
}
#[test]
fn test_error_display() {
assert_eq!(HttpError::ConnectTimeout.to_string(), "连接超时");
assert_eq!(HttpError::ServerError(500).to_string(), "服务器错误: 500");
assert!(HttpError::InvalidUrl("bad".into())
.to_string()
.contains("bad"));
}
#[test]
fn test_status_code() {
assert_eq!(HttpError::ServerError(500).status_code(), Some(500));
assert_eq!(
HttpError::ClientError(404, "Not Found".into()).status_code(),
Some(404)
);
assert_eq!(HttpError::ConnectTimeout.status_code(), None);
}
}