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("offline, and not in the cache")]
49 Offline,
50
51 #[error("provider temporarily disabled after repeated failures")]
53 CircuitOpen { until: Option<Timestamp> },
54
55 #[error("authentication rejected{}", message_hint(.message))]
57 AuthRejected { message: Option<String> },
58
59 #[error("not found")]
62 NotFound,
63
64 #[error("bad request: {0}")]
66 BadRequest(String),
67
68 #[error("unsupported: {0}")]
70 Unsupported(&'static str),
71
72 #[error("could not decode response: {0}")]
74 Decode(#[source] Source),
75
76 #[error(transparent)]
77 InvalidId(#[from] IdError),
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83#[non_exhaustive]
84pub enum ErrorCategory {
85 Unavailable,
87 Throttled,
89 Auth,
91 NotFound,
92 Usage,
94 Unsupported,
96 Protocol,
99}
100
101impl Error {
102 pub fn network(e: impl std::error::Error + Send + Sync + 'static) -> Self {
103 Error::Network(Arc::new(e))
104 }
105
106 pub fn decode(e: impl std::error::Error + Send + Sync + 'static) -> Self {
107 Error::Decode(Arc::new(e))
108 }
109
110 pub fn is_retryable(&self) -> bool {
113 matches!(
114 self,
115 Error::RateLimited { .. } | Error::Network(_) | Error::Timeout | Error::Server { .. }
116 )
117 }
118
119 pub fn trips_breaker(&self) -> bool {
122 matches!(
123 self,
124 Error::Network(_) | Error::Timeout | Error::Server { .. }
125 )
126 }
127
128 pub fn category(&self) -> ErrorCategory {
129 match self {
130 Error::Network(_)
131 | Error::Timeout
132 | Error::Server { .. }
133 | Error::CircuitOpen { .. }
134 | Error::Offline => ErrorCategory::Unavailable,
135 Error::RateLimited { .. } | Error::QuotaExhausted { .. } => ErrorCategory::Throttled,
136 Error::AuthRejected { .. } => ErrorCategory::Auth,
137 Error::NotFound => ErrorCategory::NotFound,
138 Error::BadRequest(_) | Error::InvalidId(_) => ErrorCategory::Usage,
139 Error::Unsupported(_) => ErrorCategory::Unsupported,
140 Error::Decode(_) => ErrorCategory::Protocol,
141 }
142 }
143}
144
145fn retry_hint(d: &Option<Duration>) -> String {
146 d.map(|d| format!(" (retry after {}s)", d.as_secs()))
147 .unwrap_or_default()
148}
149
150fn reset_hint(t: &Option<Timestamp>) -> String {
151 t.map(|t| format!(" (resets at {t})")).unwrap_or_default()
152}
153
154fn message_hint(m: &Option<String>) -> String {
155 m.as_ref().map(|m| format!(": {m}")).unwrap_or_default()
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn rate_limits_retry_but_do_not_trip_the_breaker() {
164 let e = Error::RateLimited { retry_after: None };
165 assert!(e.is_retryable());
166 assert!(!e.trips_breaker());
167 assert_eq!(e.category(), ErrorCategory::Throttled);
168 }
169
170 #[test]
171 fn quota_and_auth_are_not_retryable() {
172 assert!(!Error::QuotaExhausted { resets_at: None }.is_retryable());
173 assert!(!Error::AuthRejected { message: None }.is_retryable());
174 }
175
176 #[test]
177 fn server_errors_retry_and_trip() {
178 let e = Error::Server {
179 status: 503,
180 message: Some("Service Unavailable".into()),
181 };
182 assert!(e.is_retryable() && e.trips_breaker());
183 assert_eq!(
184 e.to_string(),
185 "server error (HTTP 503): Service Unavailable"
186 );
187 }
188
189 #[test]
190 fn errors_are_cloneable_with_sources() {
191 let e = Error::network(std::io::Error::other("reset"));
192 let shared = e.clone();
193 assert_eq!(shared.to_string(), "network error: reset");
194 assert!(std::error::Error::source(&shared).is_some());
195 }
196}