use std::borrow::Cow;
use std::fmt;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum HttpErrorKind {
ClientError(u16),
ServerError(u16),
RateLimited,
NetworkError,
TlsError,
InvalidUrl,
RedirectLoop,
TooManyRedirects,
RequestTimeout,
EncodingError,
DecodingError,
}
impl HttpErrorKind {
#[inline]
pub fn is_retryable(&self) -> bool {
match self {
HttpErrorKind::ClientError(_) => false,
HttpErrorKind::ServerError(_) => true,
HttpErrorKind::RateLimited => true,
HttpErrorKind::NetworkError => true,
HttpErrorKind::TlsError => true,
HttpErrorKind::InvalidUrl => false,
HttpErrorKind::RedirectLoop => false,
HttpErrorKind::TooManyRedirects => false,
HttpErrorKind::RequestTimeout => true,
HttpErrorKind::EncodingError => true,
HttpErrorKind::DecodingError => false,
}
}
#[inline]
pub fn status_code(&self) -> Option<u16> {
match self {
HttpErrorKind::ClientError(code) | HttpErrorKind::ServerError(code) => Some(*code),
_ => None,
}
}
#[inline]
pub fn from_status(status: u16) -> Self {
match status {
429 => Self::RateLimited,
400..=499 => Self::ClientError(status),
500..=599 => Self::ServerError(status),
_ if status >= 400 => Self::ClientError(status),
_ => Self::ServerError(status),
}
}
#[inline]
pub fn is_client_error(&self) -> bool {
matches!(self, HttpErrorKind::ClientError(_))
}
#[inline]
pub fn is_server_error(&self) -> bool {
matches!(self, HttpErrorKind::ServerError(_))
}
#[inline]
pub fn is_error(&self) -> bool {
self.status_code().is_some_and(|s| s >= 400)
}
#[inline]
pub fn is_success(&self) -> bool {
false }
#[inline]
pub fn status_range_description(&self) -> &str {
match self {
HttpErrorKind::ClientError(_) => "Client Error (4xx)",
HttpErrorKind::ServerError(_) => "Server Error (5xx)",
HttpErrorKind::RateLimited => "Rate Limited (429)",
HttpErrorKind::NetworkError => "Network Error",
HttpErrorKind::TlsError => "TLS/SSL Error",
HttpErrorKind::InvalidUrl => "Invalid URL",
HttpErrorKind::RedirectLoop => "Redirect Loop",
HttpErrorKind::TooManyRedirects => "Too Many Redirects",
HttpErrorKind::RequestTimeout => "Request Timeout",
HttpErrorKind::EncodingError => "Encoding Error",
HttpErrorKind::DecodingError => "Decoding Error",
}
}
#[inline]
pub fn to_machine_string(&self) -> Cow<'static, str> {
match self {
HttpErrorKind::ClientError(code) => Cow::Owned(format!("client_error_{code}")),
HttpErrorKind::ServerError(code) => Cow::Owned(format!("server_error_{code}")),
HttpErrorKind::RateLimited => Cow::Borrowed("rate_limited"),
HttpErrorKind::NetworkError => Cow::Borrowed("network_error"),
HttpErrorKind::TlsError => Cow::Borrowed("tls_error"),
HttpErrorKind::InvalidUrl => Cow::Borrowed("invalid_url"),
HttpErrorKind::RedirectLoop => Cow::Borrowed("redirect_loop"),
HttpErrorKind::TooManyRedirects => Cow::Borrowed("too_many_redirects"),
HttpErrorKind::RequestTimeout => Cow::Borrowed("request_timeout"),
HttpErrorKind::EncodingError => Cow::Borrowed("encoding_error"),
HttpErrorKind::DecodingError => Cow::Borrowed("decoding_error"),
}
}
}
impl fmt::Display for HttpErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HttpErrorKind::ClientError(code) => write!(f, "client error: {code}"),
HttpErrorKind::ServerError(code) => write!(f, "server error: {code}"),
HttpErrorKind::RateLimited => write!(f, "rate limited"),
HttpErrorKind::NetworkError => write!(f, "network error"),
HttpErrorKind::TlsError => write!(f, "TLS error"),
HttpErrorKind::InvalidUrl => write!(f, "invalid URL"),
HttpErrorKind::RedirectLoop => write!(f, "redirect loop"),
HttpErrorKind::TooManyRedirects => write!(f, "too many redirects"),
HttpErrorKind::RequestTimeout => write!(f, "request timeout"),
HttpErrorKind::EncodingError => write!(f, "encoding error"),
HttpErrorKind::DecodingError => write!(f, "decoding error"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_error_not_retryable() {
let kind = HttpErrorKind::ClientError(400);
assert!(!kind.is_retryable());
assert_eq!(kind.status_code(), Some(400));
}
#[test]
fn test_server_error_retryable() {
let kind = HttpErrorKind::ServerError(500);
assert!(kind.is_retryable());
assert_eq!(kind.status_code(), Some(500));
}
#[test]
fn test_rate_limited_retryable() {
let kind = HttpErrorKind::RateLimited;
assert!(kind.is_retryable());
assert_eq!(kind.status_code(), None);
}
#[test]
fn test_display() {
assert_eq!(
HttpErrorKind::ClientError(400).to_string(),
"client error: 400"
);
assert_eq!(
HttpErrorKind::ServerError(500).to_string(),
"server error: 500"
);
assert_eq!(HttpErrorKind::RateLimited.to_string(), "rate limited");
assert_eq!(HttpErrorKind::NetworkError.to_string(), "network error");
}
}