1use std::sync::Arc;
12use std::time::Duration;
13
14use jiff::Timestamp;
15
16use crate::id::IdError;
17
18pub type Result<T, E = Error> = std::result::Result<T, E>;
19
20pub type Source = Arc<dyn std::error::Error + Send + Sync>;
22
23#[derive(Debug, Clone, thiserror::Error)]
24#[non_exhaustive]
25pub enum Error {
26 #[error("rate limited{}", retry_hint(.retry_after))]
28 RateLimited { retry_after: Option<Duration> },
29
30 #[error("usage quota exhausted{}", reset_hint(.resets_at))]
33 QuotaExhausted { resets_at: Option<Timestamp> },
34
35 #[error("network error: {0}")]
36 Network(#[source] Source),
37
38 #[error("request timed out")]
39 Timeout,
40
41 #[error("server error (HTTP {status}){}", message_hint(.message))]
42 Server {
43 status: u16,
44 message: Option<String>,
45 },
46
47 #[error("provider temporarily disabled after repeated failures")]
49 CircuitOpen { until: Option<Timestamp> },
50
51 #[error("authentication rejected{}", message_hint(.message))]
53 AuthRejected { message: Option<String> },
54
55 #[error("not found")]
58 NotFound,
59
60 #[error("bad request: {0}")]
62 BadRequest(String),
63
64 #[error("unsupported: {0}")]
66 Unsupported(&'static str),
67
68 #[error("could not decode response: {0}")]
70 Decode(#[source] Source),
71
72 #[error(transparent)]
73 InvalidId(#[from] IdError),
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
79#[non_exhaustive]
80pub enum ErrorCategory {
81 Unavailable,
83 Throttled,
85 Auth,
87 NotFound,
88 Usage,
90 Unsupported,
92 Protocol,
95}
96
97impl Error {
98 pub fn network(e: impl std::error::Error + Send + Sync + 'static) -> Self {
99 Error::Network(Arc::new(e))
100 }
101
102 pub fn decode(e: impl std::error::Error + Send + Sync + 'static) -> Self {
103 Error::Decode(Arc::new(e))
104 }
105
106 pub fn is_retryable(&self) -> bool {
109 matches!(
110 self,
111 Error::RateLimited { .. } | Error::Network(_) | Error::Timeout | Error::Server { .. }
112 )
113 }
114
115 pub fn trips_breaker(&self) -> bool {
118 matches!(
119 self,
120 Error::Network(_) | Error::Timeout | Error::Server { .. }
121 )
122 }
123
124 pub fn category(&self) -> ErrorCategory {
125 match self {
126 Error::Network(_)
127 | Error::Timeout
128 | Error::Server { .. }
129 | Error::CircuitOpen { .. } => ErrorCategory::Unavailable,
130 Error::RateLimited { .. } | Error::QuotaExhausted { .. } => ErrorCategory::Throttled,
131 Error::AuthRejected { .. } => ErrorCategory::Auth,
132 Error::NotFound => ErrorCategory::NotFound,
133 Error::BadRequest(_) | Error::InvalidId(_) => ErrorCategory::Usage,
134 Error::Unsupported(_) => ErrorCategory::Unsupported,
135 Error::Decode(_) => ErrorCategory::Protocol,
136 }
137 }
138}
139
140fn retry_hint(d: &Option<Duration>) -> String {
141 d.map(|d| format!(" (retry after {}s)", d.as_secs()))
142 .unwrap_or_default()
143}
144
145fn reset_hint(t: &Option<Timestamp>) -> String {
146 t.map(|t| format!(" (resets at {t})")).unwrap_or_default()
147}
148
149fn message_hint(m: &Option<String>) -> String {
150 m.as_ref().map(|m| format!(": {m}")).unwrap_or_default()
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn rate_limits_retry_but_do_not_trip_the_breaker() {
159 let e = Error::RateLimited { retry_after: None };
160 assert!(e.is_retryable());
161 assert!(!e.trips_breaker());
162 assert_eq!(e.category(), ErrorCategory::Throttled);
163 }
164
165 #[test]
166 fn quota_and_auth_are_not_retryable() {
167 assert!(!Error::QuotaExhausted { resets_at: None }.is_retryable());
168 assert!(!Error::AuthRejected { message: None }.is_retryable());
169 }
170
171 #[test]
172 fn server_errors_retry_and_trip() {
173 let e = Error::Server {
174 status: 503,
175 message: Some("Service Unavailable".into()),
176 };
177 assert!(e.is_retryable() && e.trips_breaker());
178 assert_eq!(
179 e.to_string(),
180 "server error (HTTP 503): Service Unavailable"
181 );
182 }
183
184 #[test]
185 fn errors_are_cloneable_with_sources() {
186 let e = Error::network(std::io::Error::other("reset"));
187 let shared = e.clone();
188 assert_eq!(shared.to_string(), "network error: reset");
189 assert!(std::error::Error::source(&shared).is_some());
190 }
191}