1use backon::ExponentialBuilder;
9
10#[must_use]
27pub fn is_retryable_http(status: u16) -> bool {
28 matches!(status, 429 | 500 | 502 | 503 | 504)
29}
30
31#[must_use]
46pub(crate) fn is_retryable_octocrab(e: &octocrab::Error) -> bool {
47 match e {
48 octocrab::Error::GitHub { source, .. } => {
49 matches!(
52 source.status_code.as_u16(),
53 429 | 500 | 502 | 503 | 504 | 403
54 )
55 }
56 octocrab::Error::Service { .. } | octocrab::Error::Hyper { .. } => true,
57 _ => false,
58 }
59}
60
61#[must_use]
74pub fn is_retryable_anyhow(e: &anyhow::Error) -> bool {
75 if let Some(oct_err) = e.downcast_ref::<octocrab::Error>() {
77 return is_retryable_octocrab(oct_err);
78 }
79
80 if let Some(req_err) = e.downcast_ref::<reqwest::Error>() {
82 if req_err.is_timeout() || req_err.is_connect() {
84 return true;
85 }
86 if let Some(status) = req_err.status() {
88 return is_retryable_http(status.as_u16());
89 }
90 }
91
92 if let Some(aptu_err) = e.downcast_ref::<crate::error::AptuError>() {
94 return matches!(
95 aptu_err,
96 crate::error::AptuError::RateLimited { .. }
97 | crate::error::AptuError::TruncatedResponse { .. }
98 );
99 }
100
101 false
102}
103
104#[must_use]
116pub fn retry_backoff() -> ExponentialBuilder {
117 ExponentialBuilder::default()
118 .with_factor(2.0)
119 .with_min_delay(std::time::Duration::from_secs(1))
120 .with_max_times(3)
121 .with_jitter()
122}
123
124const MAX_RETRY_AFTER_SECS: u64 = 120;
126
127#[must_use]
142pub fn extract_retry_after(e: &anyhow::Error) -> Option<std::time::Duration> {
143 if let Some(crate::error::AptuError::RateLimited { retry_after, .. }) =
144 e.downcast_ref::<crate::error::AptuError>()
145 && *retry_after > 0
146 {
147 let capped = (*retry_after).min(MAX_RETRY_AFTER_SECS);
148 return Some(std::time::Duration::from_secs(capped));
149 }
150 None
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn test_is_retryable_http_429() {
159 assert!(is_retryable_http(429));
160 }
161
162 #[test]
163 fn test_is_retryable_http_500() {
164 assert!(is_retryable_http(500));
165 }
166
167 #[test]
168 fn test_is_retryable_http_502() {
169 assert!(is_retryable_http(502));
170 }
171
172 #[test]
173 fn test_is_retryable_http_503() {
174 assert!(is_retryable_http(503));
175 }
176
177 #[test]
178 fn test_is_retryable_http_504() {
179 assert!(is_retryable_http(504));
180 }
181
182 #[test]
183 fn test_is_retryable_http_non_retryable() {
184 assert!(!is_retryable_http(400));
185 assert!(!is_retryable_http(401));
186 assert!(!is_retryable_http(403));
187 assert!(!is_retryable_http(404));
188 assert!(!is_retryable_http(200));
189 assert!(!is_retryable_http(201));
190 }
191
192 #[test]
193 fn test_retry_backoff_configuration() {
194 let backoff = retry_backoff();
195 let _: ExponentialBuilder = backoff;
197 }
198
199 #[test]
200 fn test_is_retryable_anyhow_with_non_retryable() {
201 let err = anyhow::anyhow!("some other error");
202 assert!(!is_retryable_anyhow(&err));
203 }
204
205 #[test]
206 fn test_is_retryable_http_retryable_codes() {
207 assert!(is_retryable_http(429));
208 assert!(is_retryable_http(500));
209 assert!(is_retryable_http(502));
210 assert!(is_retryable_http(503));
211 assert!(is_retryable_http(504));
212 }
213
214 #[test]
215 fn test_is_retryable_http_non_retryable_codes() {
216 assert!(!is_retryable_http(400));
217 assert!(!is_retryable_http(401));
218 assert!(!is_retryable_http(403));
219 assert!(!is_retryable_http(404));
220 assert!(!is_retryable_http(200));
221 assert!(!is_retryable_http(201));
222 }
223
224 #[test]
225 fn test_is_retryable_anyhow_with_truncated_response() {
226 let err = anyhow::anyhow!(crate::error::AptuError::TruncatedResponse {
227 provider: "OpenRouter".to_string(),
228 });
229 assert!(is_retryable_anyhow(&err));
230 }
231
232 #[test]
233 fn test_is_retryable_anyhow_with_rate_limited() {
234 let err = anyhow::anyhow!(crate::error::AptuError::RateLimited {
235 provider: "OpenRouter".to_string(),
236 retry_after: 60,
237 });
238 assert!(is_retryable_anyhow(&err));
239 }
240
241 #[test]
242 fn test_extract_retry_after_with_valid_value() {
243 let err = anyhow::anyhow!(crate::error::AptuError::RateLimited {
244 provider: "OpenRouter".to_string(),
245 retry_after: 60,
246 });
247 let duration = extract_retry_after(&err);
248 assert_eq!(duration, Some(std::time::Duration::from_secs(60)));
249 }
250
251 #[test]
252 fn test_extract_retry_after_with_zero_value() {
253 let err = anyhow::anyhow!(crate::error::AptuError::RateLimited {
254 provider: "OpenRouter".to_string(),
255 retry_after: 0,
256 });
257 let duration = extract_retry_after(&err);
258 assert_eq!(duration, None);
259 }
260
261 #[test]
262 fn test_extract_retry_after_with_capped_value() {
263 let err = anyhow::anyhow!(crate::error::AptuError::RateLimited {
264 provider: "OpenRouter".to_string(),
265 retry_after: 300,
266 });
267 let duration = extract_retry_after(&err);
268 assert_eq!(duration, Some(std::time::Duration::from_secs(120)));
269 }
270
271 #[test]
272 fn test_extract_retry_after_with_non_rate_limited_error() {
273 let err = anyhow::anyhow!("some other error");
274 let duration = extract_retry_after(&err);
275 assert_eq!(duration, None);
276 }
277
278 #[test]
279 fn test_extract_retry_after_with_truncated_response() {
280 let err = anyhow::anyhow!(crate::error::AptuError::TruncatedResponse {
281 provider: "OpenRouter".to_string(),
282 });
283 let duration = extract_retry_after(&err);
284 assert_eq!(duration, None);
285 }
286}