1use std::time::Duration;
11
12#[derive(Debug, Clone)]
14pub enum BackendErrorKind {
15 Timeout,
17 RateLimit {
19 retry_after: Option<Duration>,
21 },
22 ServerError { status: u16 },
24 AuthError,
26 NetworkError,
28 StreamDropped,
30 InvalidResponse,
32 ProviderUnavailable,
34 CircuitOpen,
36 Unknown,
38}
39
40impl BackendErrorKind {
41 pub fn is_retryable(&self) -> bool {
43 matches!(
44 self,
45 BackendErrorKind::Timeout
46 | BackendErrorKind::RateLimit { .. }
47 | BackendErrorKind::ServerError { .. }
48 | BackendErrorKind::NetworkError
49 | BackendErrorKind::StreamDropped
50 )
51 }
52
53 pub fn category(&self) -> &'static str {
55 match self {
56 BackendErrorKind::Timeout => "timeout",
57 BackendErrorKind::RateLimit { .. } => "rate_limit",
58 BackendErrorKind::ServerError { .. } => "server_error",
59 BackendErrorKind::AuthError => "auth_error",
60 BackendErrorKind::NetworkError => "network_error",
61 BackendErrorKind::StreamDropped => "stream_dropped",
62 BackendErrorKind::InvalidResponse => "invalid_response",
63 BackendErrorKind::ProviderUnavailable => "provider_unavailable",
64 BackendErrorKind::CircuitOpen => "circuit_open",
65 BackendErrorKind::Unknown => "unknown",
66 }
67 }
68
69 pub fn from_status(status: u16) -> Self {
71 match status {
72 401 | 403 => BackendErrorKind::AuthError,
73 429 => BackendErrorKind::RateLimit { retry_after: None },
74 408 => BackendErrorKind::Timeout,
75 s if s >= 500 => BackendErrorKind::ServerError { status: s },
76 _ => BackendErrorKind::Unknown,
77 }
78 }
79
80 pub fn from_reqwest_error(e: &reqwest::Error) -> Self {
82 if e.is_timeout() {
83 BackendErrorKind::Timeout
84 } else if e.is_connect() {
85 BackendErrorKind::NetworkError
86 } else if e.is_request() {
87 BackendErrorKind::NetworkError
88 } else if let Some(status) = e.status() {
89 BackendErrorKind::from_status(status.as_u16())
90 } else {
91 BackendErrorKind::NetworkError
92 }
93 }
94}
95
96impl std::fmt::Display for BackendErrorKind {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 write!(f, "{}", self.category())
99 }
100}
101
102#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn test_retryable_errors() {
110 assert!(BackendErrorKind::Timeout.is_retryable());
111 assert!(BackendErrorKind::RateLimit { retry_after: None }.is_retryable());
112 assert!(BackendErrorKind::ServerError { status: 500 }.is_retryable());
113 assert!(BackendErrorKind::ServerError { status: 503 }.is_retryable());
114 assert!(BackendErrorKind::NetworkError.is_retryable());
115 assert!(BackendErrorKind::StreamDropped.is_retryable());
116 }
117
118 #[test]
119 fn test_non_retryable_errors() {
120 assert!(!BackendErrorKind::AuthError.is_retryable());
121 assert!(!BackendErrorKind::InvalidResponse.is_retryable());
122 assert!(!BackendErrorKind::ProviderUnavailable.is_retryable());
123 assert!(!BackendErrorKind::CircuitOpen.is_retryable());
124 assert!(!BackendErrorKind::Unknown.is_retryable());
125 }
126
127 #[test]
128 fn test_from_status_classification() {
129 assert!(matches!(BackendErrorKind::from_status(401), BackendErrorKind::AuthError));
130 assert!(matches!(BackendErrorKind::from_status(403), BackendErrorKind::AuthError));
131 assert!(matches!(BackendErrorKind::from_status(429), BackendErrorKind::RateLimit { .. }));
132 assert!(matches!(BackendErrorKind::from_status(408), BackendErrorKind::Timeout));
133 assert!(matches!(BackendErrorKind::from_status(500), BackendErrorKind::ServerError { status: 500 }));
134 assert!(matches!(BackendErrorKind::from_status(502), BackendErrorKind::ServerError { status: 502 }));
135 assert!(matches!(BackendErrorKind::from_status(503), BackendErrorKind::ServerError { status: 503 }));
136 assert!(matches!(BackendErrorKind::from_status(400), BackendErrorKind::Unknown));
137 assert!(matches!(BackendErrorKind::from_status(404), BackendErrorKind::Unknown));
138 }
139
140 #[test]
141 fn test_category_strings() {
142 assert_eq!(BackendErrorKind::Timeout.category(), "timeout");
143 assert_eq!(BackendErrorKind::RateLimit { retry_after: None }.category(), "rate_limit");
144 assert_eq!(BackendErrorKind::AuthError.category(), "auth_error");
145 assert_eq!(BackendErrorKind::CircuitOpen.category(), "circuit_open");
146 }
147
148 #[test]
149 fn test_display() {
150 assert_eq!(format!("{}", BackendErrorKind::Timeout), "timeout");
151 assert_eq!(format!("{}", BackendErrorKind::NetworkError), "network_error");
152 }
153}