1use std::{
4 collections::BTreeSet,
5 fmt,
6 sync::Arc,
7 time::{Duration, Instant, SystemTime},
8};
9
10use reqwest::header::HeaderMap;
11
12use crate::Error;
13
14pub type RetryPredicate = Arc<dyn Fn(&Error) -> bool + Send + Sync>;
16
17#[derive(Clone)]
23pub struct RetryPolicy {
24 pub max_retries: u32,
26 pub backoff_initial: Duration,
28 pub backoff_max: Duration,
30 pub backoff_jitter: f64,
32 pub http_statuses: BTreeSet<u16>,
34 pub respect_retry_after: bool,
36 pub max_retry_after: Option<Duration>,
39 pub api_connection_error: bool,
41 pub api_timeout_error: bool,
43 pub predicate: Option<RetryPredicate>,
46 pub timeout: Option<Duration>,
48}
49
50impl Default for RetryPolicy {
51 fn default() -> Self {
52 Self {
53 max_retries: 2,
54 backoff_initial: Duration::from_millis(500),
55 backoff_max: Duration::from_secs(5),
56 backoff_jitter: 0.25,
57 http_statuses: [408, 429].into_iter().chain(500..600).collect(),
58 respect_retry_after: true,
59 max_retry_after: Some(Duration::from_secs(60)),
60 api_connection_error: true,
61 api_timeout_error: true,
62 predicate: None,
63 timeout: Some(Duration::from_secs(30)),
64 }
65 }
66}
67
68impl fmt::Debug for RetryPolicy {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 f.debug_struct("RetryPolicy")
71 .field("max_retries", &self.max_retries)
72 .field("backoff_initial", &self.backoff_initial)
73 .field("backoff_max", &self.backoff_max)
74 .field("backoff_jitter", &self.backoff_jitter)
75 .field("http_statuses", &self.http_statuses)
76 .field("respect_retry_after", &self.respect_retry_after)
77 .field("max_retry_after", &self.max_retry_after)
78 .field("api_connection_error", &self.api_connection_error)
79 .field("api_timeout_error", &self.api_timeout_error)
80 .field("predicate", &self.predicate.as_ref().map(|_| "<predicate>"))
81 .field("timeout", &self.timeout)
82 .finish()
83 }
84}
85
86impl RetryPolicy {
87 pub fn no_retries() -> Self {
89 Self {
90 max_retries: 0,
91 ..Self::default()
92 }
93 }
94
95 pub fn validate(&self) -> Result<(), Error> {
97 if !(0.0..=1.0).contains(&self.backoff_jitter) {
98 return Err(Error::Configuration(
99 "backoff_jitter must be between zero and one".into(),
100 ));
101 }
102 if self.timeout.is_some_and(|timeout| timeout.is_zero()) {
103 return Err(Error::Configuration(
104 "retry timeout must be greater than zero".into(),
105 ));
106 }
107 if self
108 .http_statuses
109 .iter()
110 .any(|status| !(100..=999).contains(status))
111 {
112 return Err(Error::Configuration(
113 "retry http_statuses must contain HTTP codes between 100 and 999".into(),
114 ));
115 }
116 let now = Instant::now();
117 for (name, duration) in [
118 ("backoff_initial", Some(self.backoff_initial)),
119 ("backoff_max", Some(self.backoff_max)),
120 ("max_retry_after", self.max_retry_after),
121 ("retry timeout", self.timeout),
122 ] {
123 if duration.is_some_and(|duration| now.checked_add(duration).is_none()) {
124 return Err(Error::Configuration(format!(
125 "{name} is too large for the platform's timer"
126 )));
127 }
128 }
129 Ok(())
130 }
131
132 pub fn should_retry(&self, error: &Error) -> bool {
137 let builtin = match error {
138 Error::Cancelled => return false,
139 Error::Timeout { .. } => self.api_timeout_error,
140 Error::Connection(_) => self.api_connection_error,
141 Error::Api(error) => self.http_statuses.contains(&error.status.as_u16()),
142 Error::ResponseValidation { response, .. } => {
143 self.http_statuses.contains(&response.status.as_u16())
144 }
145 _ => false,
146 };
147 builtin
148 || self
149 .predicate
150 .as_ref()
151 .is_some_and(|predicate| predicate(error))
152 }
153
154 pub fn delay(&self, attempt: u32, headers: Option<&HeaderMap>) -> Duration {
160 if self.respect_retry_after {
161 if let Some(delay) = headers.and_then(parse_retry_after) {
162 if self.max_retry_after.is_none_or(|maximum| delay <= maximum) {
163 return delay;
164 }
165 }
166 }
167 self.backoff(attempt, fastrand::f64())
168 }
169
170 fn backoff(&self, attempt: u32, random: f64) -> Duration {
171 let nanoseconds = self
172 .backoff_initial
173 .as_nanos()
174 .saturating_mul(2_u128.saturating_pow(attempt))
175 .min(self.backoff_max.as_nanos());
176 let exponential = Duration::new(
177 (nanoseconds / 1_000_000_000) as u64,
178 (nanoseconds % 1_000_000_000) as u32,
179 );
180 let seconds = exponential.as_secs_f64() * (1.0 - random * self.backoff_jitter);
181 Duration::try_from_secs_f64(seconds)
182 .unwrap_or(exponential)
183 .min(exponential)
184 }
185}
186
187pub fn parse_retry_after(headers: &HeaderMap) -> Option<Duration> {
193 parse_retry_after_at(headers, SystemTime::now())
194}
195
196fn parse_retry_after_at(headers: &HeaderMap, now: SystemTime) -> Option<Duration> {
197 if let Some(delay) = headers
198 .get("retry-after-ms")
199 .and_then(|value| value.to_str().ok())
200 .and_then(|raw| parse_seconds(raw, 0.001))
201 {
202 return Some(delay);
203 }
204 let raw = headers.get("retry-after")?.to_str().ok()?.trim();
205 if let Some(delay) = parse_seconds(raw, 1.0) {
206 return Some(delay);
207 }
208 let date = httpdate::parse_http_date(raw).ok()?;
209 timer_duration(date.duration_since(now).unwrap_or(Duration::ZERO))
210}
211
212fn parse_seconds(raw: &str, multiplier: f64) -> Option<Duration> {
213 let raw = raw.trim();
214 let value = if raw.is_empty() {
215 0.0
216 } else {
217 raw.parse::<f64>().ok()?
218 };
219 if !value.is_finite() || value < 0.0 {
220 return None;
221 }
222 timer_duration(Duration::try_from_secs_f64(value * multiplier).ok()?)
223}
224
225fn timer_duration(duration: Duration) -> Option<Duration> {
226 Instant::now().checked_add(duration).map(|_| duration)
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use crate::ApiError;
233 use reqwest::StatusCode;
234 use serde_json::Value;
235
236 fn headers(ms: Option<&str>, seconds: Option<&str>) -> HeaderMap {
237 let mut headers = HeaderMap::new();
238 if let Some(ms) = ms {
239 headers.insert("retry-after-ms", ms.parse().unwrap());
240 }
241 if let Some(seconds) = seconds {
242 headers.insert("retry-after", seconds.parse().unwrap());
243 }
244 headers
245 }
246
247 #[test]
248 fn parses_fractional_delays_and_prefers_milliseconds() {
249 assert_eq!(
250 parse_retry_after(&headers(Some("250.5"), Some("3"))),
251 Some(Duration::from_micros(250_500))
252 );
253 assert_eq!(
254 parse_retry_after(&headers(None, Some("1.25"))),
255 Some(Duration::from_millis(1250))
256 );
257 assert_eq!(
258 parse_retry_after(&headers(Some(" "), Some("3"))),
259 Some(Duration::ZERO)
260 );
261 assert_eq!(
262 parse_retry_after(&headers(Some("invalid"), Some("3"))),
263 Some(Duration::from_secs(3))
264 );
265 }
266
267 #[test]
268 fn handles_hostile_header_numbers_without_panicking() {
269 for raw in ["NaN", "inf", "-inf", "1e300", "-1", "1e99999999999"] {
270 assert_eq!(parse_retry_after(&headers(None, Some(raw))), None, "{raw}");
271 assert_eq!(
272 parse_retry_after(&headers(Some(raw), Some("2"))),
273 Some(Duration::from_secs(2)),
274 "{raw}"
275 );
276 }
277 assert_eq!(
278 parse_retry_after(&headers(None, Some("18446744073709549568"))),
279 None
280 );
281 }
282
283 #[test]
284 fn parses_http_dates_and_clamps_past_dates() {
285 let date = "Wed, 21 Oct 2015 07:28:00 GMT";
286 let timestamp = httpdate::parse_http_date(date).unwrap();
287 let headers = headers(None, Some(date));
288 assert_eq!(
289 parse_retry_after_at(&headers, timestamp - Duration::from_secs(5)),
290 Some(Duration::from_secs(5))
291 );
292 assert_eq!(
293 parse_retry_after_at(&headers, timestamp + Duration::from_secs(5)),
294 Some(Duration::ZERO)
295 );
296 }
297
298 #[test]
299 fn exponential_backoff_caps_and_supports_disabled_waits() {
300 let mut policy = RetryPolicy {
301 backoff_jitter: 0.0,
302 ..RetryPolicy::default()
303 };
304 for (attempt, milliseconds) in [(0, 500), (1, 1000), (2, 2000), (4, 5000), (u32::MAX, 5000)]
305 {
306 assert_eq!(
307 policy.delay(attempt, None),
308 Duration::from_millis(milliseconds)
309 );
310 }
311 policy.backoff_initial = Duration::ZERO;
312 assert_eq!(policy.delay(u32::MAX, None), Duration::ZERO);
313 policy.backoff_initial = Duration::MAX;
314 policy.backoff_max = Duration::MAX;
315 assert_eq!(policy.delay(u32::MAX, None), Duration::MAX);
316 policy.backoff_max = Duration::ZERO;
317 assert_eq!(policy.delay(u32::MAX, None), Duration::ZERO);
318 }
319
320 #[test]
321 fn jitter_only_reduces_capped_backoff() {
322 let policy = RetryPolicy::default();
323 assert_eq!(policy.backoff(0, 0.0), Duration::from_millis(500));
324 assert_eq!(policy.backoff(0, 1.0), Duration::from_millis(375));
325 assert_eq!(policy.backoff(10, 1.0), Duration::from_millis(3750));
326 }
327
328 #[test]
329 fn server_hints_respect_the_configured_limit() {
330 let mut policy = RetryPolicy {
331 backoff_jitter: 0.0,
332 ..RetryPolicy::default()
333 };
334 assert_eq!(
335 policy.delay(0, Some(&headers(None, Some("60")))),
336 Duration::from_secs(60)
337 );
338 assert_eq!(
339 policy.delay(0, Some(&headers(None, Some("61")))),
340 Duration::from_millis(500)
341 );
342 policy.max_retry_after = None;
343 assert_eq!(
344 policy.delay(0, Some(&headers(None, Some("61")))),
345 Duration::from_secs(61)
346 );
347 policy.respect_retry_after = false;
348 assert_eq!(
349 policy.delay(0, Some(&headers(None, Some("10")))),
350 Duration::from_millis(500)
351 );
352 }
353
354 #[test]
355 fn validates_jitter_and_budget() {
356 for jitter in [f64::NAN, f64::INFINITY, -0.01, 1.01] {
357 assert!(RetryPolicy {
358 backoff_jitter: jitter,
359 ..RetryPolicy::default()
360 }
361 .validate()
362 .is_err());
363 }
364 assert!(RetryPolicy {
365 timeout: Some(Duration::ZERO),
366 ..RetryPolicy::default()
367 }
368 .validate()
369 .is_err());
370 assert!(RetryPolicy {
371 timeout: None,
372 backoff_jitter: 1.0,
373 ..RetryPolicy::default()
374 }
375 .validate()
376 .is_ok());
377 assert!(RetryPolicy {
378 backoff_initial: Duration::MAX,
379 ..RetryPolicy::default()
380 }
381 .validate()
382 .is_err());
383 assert!(RetryPolicy {
384 backoff_max: Duration::MAX,
385 ..RetryPolicy::default()
386 }
387 .validate()
388 .is_err());
389 assert!(RetryPolicy {
390 max_retry_after: Some(Duration::MAX),
391 ..RetryPolicy::default()
392 }
393 .validate()
394 .is_err());
395 assert!(RetryPolicy {
396 timeout: Some(Duration::MAX),
397 ..RetryPolicy::default()
398 }
399 .validate()
400 .is_err());
401 assert!(RetryPolicy {
402 http_statuses: [99, 1000].into_iter().collect(),
403 ..RetryPolicy::default()
404 }
405 .validate()
406 .is_err());
407 }
408
409 #[test]
410 fn status_rules_and_custom_predicates_are_additive_but_never_cancelled() {
411 let mut policy = RetryPolicy::default();
412 for (status, retry) in [
413 (400, false),
414 (401, false),
415 (408, true),
416 (429, true),
417 (500, true),
418 (599, true),
419 ] {
420 let error = ApiError::new(
421 StatusCode::from_u16(status).unwrap(),
422 Value::Null,
423 HeaderMap::new(),
424 None,
425 )
426 .into();
427 assert_eq!(policy.should_retry(&error), retry, "{status}");
428 }
429 let error = Error::InvalidRequest("custom".into());
430 assert!(!policy.should_retry(&error));
431 policy.predicate = Some(Arc::new(|_| true));
432 assert!(policy.should_retry(&error));
433 assert!(!policy.should_retry(&Error::Cancelled));
434 }
435
436 #[test]
437 fn successful_validation_errors_require_explicit_retry_rules() {
438 let error = Error::ResponseValidation {
439 field_path: "answers".into(),
440 response: Box::new(crate::RawResponse {
441 status: StatusCode::OK,
442 headers: HeaderMap::new(),
443 body: Default::default(),
444 }),
445 };
446 let mut policy = RetryPolicy::default();
447 assert!(!policy.should_retry(&error));
448 policy.http_statuses.insert(200);
449 assert!(policy.should_retry(&error));
450 }
451}