1use std::time::{Duration, SystemTime, UNIX_EPOCH};
2
3#[derive(Debug, Clone, PartialEq)]
5pub struct RetryPolicy {
6 pub max_attempts: u32,
7 pub initial_backoff: Duration,
8 pub max_backoff: Duration,
9 pub multiplier: f64,
10 pub jitter: f64,
12 pub retry_statuses: Vec<u16>,
13 pub honor_retry_after: bool,
14 pub max_retry_after: Duration,
16 pub defer_over_cap: bool,
22 pub max_transport_attempts: Option<u32>,
29 pub max_status_attempts: Option<u32>,
32}
33
34impl Default for RetryPolicy {
35 fn default() -> Self {
36 Self {
37 max_attempts: 3,
38 initial_backoff: Duration::from_millis(500),
39 max_backoff: Duration::from_secs(8),
40 multiplier: 2.0,
41 jitter: 0.2,
42 retry_statuses: vec![408, 425, 429, 500, 502, 503, 504],
43 honor_retry_after: true,
44 max_retry_after: Duration::from_secs(30),
45 defer_over_cap: false,
46 max_transport_attempts: None,
47 max_status_attempts: None,
48 }
49 }
50}
51
52impl RetryPolicy {
53 pub fn defers(&self, retry_after: Option<Duration>) -> bool {
56 self.defer_over_cap
57 && self.honor_retry_after
58 && retry_after.is_some_and(|ra| ra > self.max_retry_after)
59 }
60
61 pub fn none() -> Self {
62 Self {
63 max_attempts: 1,
64 ..Self::default()
65 }
66 }
67
68 pub fn is_retryable_status(&self, status: u16) -> bool {
69 self.retry_statuses.contains(&status)
70 }
71
72 pub fn separate_budgets(&self) -> bool {
75 self.max_transport_attempts.is_some() || self.max_status_attempts.is_some()
76 }
77
78 pub fn backoff(&self, retry: u32) -> Duration {
80 let exp = retry.saturating_sub(1).min(32) as i32;
81 let secs = self.initial_backoff.as_secs_f64() * self.multiplier.powi(exp);
82 let capped = secs.min(self.max_backoff.as_secs_f64());
83 let shaved = if self.jitter > 0.0 {
84 capped * (1.0 - self.jitter.clamp(0.0, 1.0) * random_unit())
85 } else {
86 capped
87 };
88 Duration::try_from_secs_f64(shaved.max(0.0)).unwrap_or(self.max_backoff)
89 }
90
91 pub fn delay(&self, retry: u32, retry_after: Option<Duration>) -> Duration {
93 let base = self.backoff(retry);
94 match (self.honor_retry_after, retry_after) {
95 (true, Some(ra)) => base.max(ra.min(self.max_retry_after)),
96 _ => base,
97 }
98 }
99}
100
101pub(crate) struct AttemptBudgets {
108 transport: u32,
109 status: u32,
110}
111
112impl AttemptBudgets {
113 pub(crate) fn new() -> Self {
114 Self {
115 transport: 0,
116 status: 0,
117 }
118 }
119
120 fn may_retry(
121 &self,
122 policy: &RetryPolicy,
123 attempt: u32,
124 failures: u32,
125 budget: Option<u32>,
126 ) -> Option<u32> {
127 if attempt >= policy.max_attempts.max(1) {
128 return None;
129 }
130 if budget.is_some_and(|b| failures >= b) {
131 return None;
132 }
133 Some(if policy.separate_budgets() {
134 failures
135 } else {
136 attempt
137 })
138 }
139
140 pub(crate) fn transport_failed(&mut self, policy: &RetryPolicy, attempt: u32) -> Option<u32> {
142 self.transport = self.transport.saturating_add(1);
143 self.may_retry(
144 policy,
145 attempt,
146 self.transport,
147 policy.max_transport_attempts,
148 )
149 }
150
151 pub(crate) fn status_failed(&mut self, policy: &RetryPolicy, attempt: u32) -> Option<u32> {
153 self.status = self.status.saturating_add(1);
154 self.may_retry(policy, attempt, self.status, policy.max_status_attempts)
155 }
156}
157
158fn random_unit() -> f64 {
159 let mut b = [0u8; 4];
160 if getrandom::fill(&mut b).is_err() {
161 return 0.0;
162 }
163 u32::from_le_bytes(b) as f64 / u32::MAX as f64
164}
165
166pub fn parse_retry_after(value: &str) -> Option<Duration> {
169 parse_retry_after_at(value, SystemTime::now())
170}
171
172pub fn parse_retry_after_at(value: &str, now: SystemTime) -> Option<Duration> {
174 let v = value.trim();
175 if let Ok(s) = v.parse::<f64>() {
176 return Some(s)
177 .filter(|s| s.is_finite() && *s >= 0.0)
178 .map(|s| Duration::try_from_secs_f64(s).unwrap_or(Duration::MAX));
179 }
180 let at = parse_http_date(v)?;
181 Some(at.duration_since(now).unwrap_or(Duration::ZERO))
182}
183
184pub fn parse_rate_limit_reset_at(value: &str, now: SystemTime) -> Option<Duration> {
192 let v = value.trim();
193 let at = if !v.is_empty() && v.bytes().all(|b| b.is_ascii_digit()) {
194 Duration::from_secs(v.parse().ok()?)
195 } else {
196 parse_iso8601_instant(v)?
197 };
198 let now = now.duration_since(UNIX_EPOCH).unwrap_or(Duration::ZERO);
202 Some(at.saturating_sub(now))
203}
204
205pub fn parse_rate_limit_reset(value: &str) -> Option<Duration> {
207 parse_rate_limit_reset_at(value, SystemTime::now())
208}
209
210fn parse_iso8601_instant(v: &str) -> Option<Duration> {
213 let b = v.as_bytes();
214 if b.len() < 20 {
215 return None;
216 }
217 let num = |s: Option<&str>| -> Option<i64> {
218 let s = s?;
219 if !s.is_empty() && s.bytes().all(|c| c.is_ascii_digit()) {
220 s.parse().ok()
221 } else {
222 None
223 }
224 };
225 if b[4] != b'-' || b[7] != b'-' || !matches!(b[10], b'T' | b't' | b' ') {
226 return None;
227 }
228 if b[13] != b':' || b[16] != b':' {
229 return None;
230 }
231 let year = num(v.get(0..4))?;
232 let month = num(v.get(5..7))?;
233 let day = num(v.get(8..10))?;
234 let hour = num(v.get(11..13))?;
235 let minute = num(v.get(14..16))?;
236 let second = num(v.get(17..19))?;
237 if !(1..=12).contains(&month)
238 || !(1..=31).contains(&day)
239 || hour > 23
240 || minute > 59
241 || second > 60
242 {
243 return None;
244 }
245 let mut i = 19;
246 let mut nanos = 0u32;
247 let mut fraction_digits = 0usize;
248 let mut finer_nonzero = false;
249 if b.get(i) == Some(&b'.') {
250 i += 1;
251 let start = i;
252 while let Some(&digit) = b.get(i).filter(|c| c.is_ascii_digit()) {
253 if fraction_digits < 9 {
254 nanos = nanos * 10 + u32::from(digit - b'0');
255 } else if digit != b'0' {
256 finer_nonzero = true;
257 }
258 fraction_digits += 1;
259 i += 1;
260 }
261 if i == start {
262 return None;
263 }
264 for _ in fraction_digits..9 {
265 nanos *= 10;
266 }
267 if finer_nonzero {
268 if nanos == 999_999_999 {
269 nanos = 0;
270 fraction_digits = usize::MAX;
272 } else {
273 nanos += 1;
274 }
275 }
276 }
277 let offset: i64 = match *b.get(i)? {
278 b'Z' | b'z' => {
279 i += 1;
280 0
281 }
282 sign @ (b'+' | b'-') => {
283 if b.get(i + 3) != Some(&b':') {
284 return None;
285 }
286 let oh = num(v.get(i + 1..i + 3))?;
287 let om = num(v.get(i + 4..i + 6))?;
288 i += 6;
289 let secs = oh * 3600 + om * 60;
290 if sign == b'-' {
291 -secs
292 } else {
293 secs
294 }
295 }
296 _ => return None,
297 };
298 if i != b.len() {
299 return None;
300 }
301 let days = days_from_civil(year, month, day);
302 let mut secs = days * 86_400 + hour * 3_600 + minute * 60 + second - offset;
303 if secs < 0 {
304 return None;
305 }
306 if fraction_digits == usize::MAX {
307 secs = secs.checked_add(1)?;
308 }
309 Some(Duration::new(secs as u64, nanos))
310}
311
312fn month_number(m: &str) -> Option<i64> {
313 const MONTHS: [&str; 12] = [
314 "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec",
315 ];
316 let m = m.get(..3)?.to_ascii_lowercase();
317 MONTHS.iter().position(|x| *x == m).map(|i| i as i64 + 1)
318}
319
320fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
321 let y = if m <= 2 { y - 1 } else { y };
322 let era = y.div_euclid(400);
323 let yoe = y - era * 400;
324 let mp = (m + 9) % 12;
325 let doy = (153 * mp + 2) / 5 + d - 1;
326 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
327 era * 146_097 + doe - 719_468
328}
329
330pub(crate) fn parse_http_date(v: &str) -> Option<SystemTime> {
332 let t: Vec<&str> = v.split_whitespace().collect();
333 let (day, mon, year, time) = match t.len() {
334 6 if t[0].ends_with(',') && t[5].eq_ignore_ascii_case("GMT") => {
335 (t[1], t[2], t[3].parse::<i64>().ok()?, t[4])
336 }
337 4 if t[0].ends_with(',') && t[3].eq_ignore_ascii_case("GMT") => {
338 let mut p = t[1].split('-');
339 let (d, m, y) = (p.next()?, p.next()?, p.next()?);
340 if p.next().is_some() {
341 return None;
342 }
343 let yy = y.parse::<i64>().ok()?;
344 let year = if y.len() == 2 {
345 if yy < 70 {
346 2000 + yy
347 } else {
348 1900 + yy
349 }
350 } else {
351 yy
352 };
353 (d, m, year, t[2])
354 }
355 5 => (t[2], t[1], t[4].parse::<i64>().ok()?, t[3]),
356 _ => return None,
357 };
358 let day = day.parse::<i64>().ok()?;
359 let month = month_number(mon)?;
360 let mut hms = time.split(':');
361 let h = hms.next()?.parse::<i64>().ok()?;
362 let mi = hms.next()?.parse::<i64>().ok()?;
363 let s = hms.next()?.parse::<i64>().ok()?;
364 if hms.next().is_some()
365 || !(1..=31).contains(&day)
366 || !(0..24).contains(&h)
367 || !(0..60).contains(&mi)
368 || !(0..=60).contains(&s)
369 || !(1970..=9999).contains(&year)
370 {
371 return None;
372 }
373 let secs = days_from_civil(year, month, day) * 86_400 + h * 3600 + mi * 60 + s;
374 u64::try_from(secs)
375 .ok()
376 .map(|s| UNIX_EPOCH + Duration::from_secs(s))
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn huge_retry_after_saturates_instead_of_panicking() {
385 for v in [
386 "18446744073709551616",
387 "1e300",
388 "99999999999999999999999999999999999999",
389 "1.7976931348623157e308",
390 ] {
391 assert_eq!(parse_retry_after(v), Some(Duration::MAX), "{v}");
392 }
393 assert_eq!(parse_retry_after("1.5"), Some(Duration::from_millis(1500)));
394 assert_eq!(parse_retry_after("-1"), None);
395 assert_eq!(parse_retry_after("NaN"), None);
396 assert_eq!(parse_retry_after("inf"), None);
397 }
398
399 #[test]
400 fn delay_caps_a_saturated_retry_after() {
401 let p = RetryPolicy {
402 max_retry_after: Duration::from_secs(30),
403 jitter: 0.0,
404 ..RetryPolicy::default()
405 };
406 assert_eq!(p.delay(1, Some(Duration::MAX)), Duration::from_secs(30));
407 }
408
409 #[test]
410 fn backoff_with_absurd_settings_does_not_panic() {
411 let p = RetryPolicy {
412 initial_backoff: Duration::MAX,
413 max_backoff: Duration::MAX,
414 multiplier: f64::MAX,
415 jitter: 0.0,
416 ..RetryPolicy::default()
417 };
418 let _ = p.backoff(40);
419 }
420
421 #[test]
422 fn retry_after_http_date_forms() {
423 let at = UNIX_EPOCH + Duration::from_secs(784_111_777); let now = at - Duration::from_secs(90);
425 for v in [
426 "Sun, 06 Nov 1994 08:49:37 GMT",
427 "Sunday, 06-Nov-94 08:49:37 GMT",
428 "Sun Nov 6 08:49:37 1994",
429 ] {
430 assert_eq!(
431 parse_retry_after_at(v, now),
432 Some(Duration::from_secs(90)),
433 "{v}"
434 );
435 }
436 assert_eq!(
438 parse_retry_after_at("Sun, 06 Nov 1994 08:49:37 GMT", at + Duration::from_secs(5)),
439 Some(Duration::ZERO)
440 );
441 assert_eq!(parse_retry_after_at("soon", now), None);
442 assert_eq!(
443 parse_retry_after_at("Sun, 32 Nov 1994 08:49:37 GMT", now),
444 None
445 );
446 }
447
448 #[test]
449 fn retry_after_beyond_the_cap_is_kept_whole_and_defers_on_request() {
450 assert_eq!(parse_retry_after("3600"), Some(Duration::from_secs(3600)));
451 let mut p = RetryPolicy::default();
452 assert!(!p.defers(Some(Duration::from_secs(3600))));
453 p.defer_over_cap = true;
454 assert!(p.defers(Some(Duration::from_secs(3600))));
455 assert!(!p.defers(Some(Duration::from_secs(10))));
456 assert!(!p.defers(None));
457 p.honor_retry_after = false;
458 assert!(!p.defers(Some(Duration::from_secs(3600))));
459 }
460
461 #[test]
462 fn separate_budgets_do_not_steal_from_each_other_and_stay_finite() {
463 let policy = RetryPolicy {
464 max_attempts: 100,
465 max_transport_attempts: Some(2),
466 max_status_attempts: Some(3),
467 ..RetryPolicy::default()
468 };
469 let mut b = AttemptBudgets::new();
470 assert_eq!(b.transport_failed(&policy, 1), Some(1));
472 assert_eq!(b.transport_failed(&policy, 2), None);
473 assert_eq!(b.status_failed(&policy, 3), Some(1));
475 assert_eq!(b.status_failed(&policy, 4), Some(2));
476 assert_eq!(b.status_failed(&policy, 5), None);
477 assert_eq!(b.transport_failed(&policy, 6), None);
479 let capped = RetryPolicy {
481 max_attempts: 4,
482 max_transport_attempts: Some(10),
483 max_status_attempts: Some(10),
484 ..RetryPolicy::default()
485 };
486 let mut b = AttemptBudgets::new();
487 assert!(b.transport_failed(&capped, 1).is_some());
488 assert!(b.status_failed(&capped, 2).is_some());
489 assert!(b.transport_failed(&capped, 3).is_some());
490 assert_eq!(b.status_failed(&capped, 4), None);
491 let legacy = RetryPolicy {
493 max_attempts: 3,
494 ..RetryPolicy::default()
495 };
496 assert!(!legacy.separate_budgets());
497 let mut b = AttemptBudgets::new();
498 assert_eq!(b.transport_failed(&legacy, 1), Some(1));
499 assert_eq!(b.status_failed(&legacy, 2), Some(2));
500 assert_eq!(b.transport_failed(&legacy, 3), None);
501 }
502
503 #[test]
504 fn rate_limit_reset_epoch_and_iso_forms() {
505 let now = UNIX_EPOCH + Duration::from_secs(1_792_317_600 - 90);
507 let want = Some(Duration::from_secs(90));
508 assert_eq!(parse_rate_limit_reset_at("1792317600", now), want);
509 assert_eq!(parse_rate_limit_reset_at("2026-10-18T10:00:00Z", now), want);
510 assert_eq!(
511 parse_rate_limit_reset_at("2026-10-18T12:00:00+02:00", now),
512 want
513 );
514 assert_eq!(
515 parse_rate_limit_reset_at("2026-10-18 10:00:00.250Z", now),
516 Some(Duration::from_millis(90_250))
517 );
518 let at_whole_second = UNIX_EPOCH + Duration::from_secs(1_792_317_600);
519 assert_eq!(
520 parse_rate_limit_reset_at("2026-10-18T10:00:00.5Z", at_whole_second),
521 Some(Duration::from_millis(500))
522 );
523 assert_eq!(
524 parse_rate_limit_reset_at("2026-10-18T10:00:00.0000000001Z", at_whole_second),
525 Some(Duration::from_nanos(1))
526 );
527 let tick = if cfg!(windows) { 100 } else { 1 };
530 assert_eq!(
531 parse_rate_limit_reset_at(
532 "2026-10-18T10:00:00.0000000001Z",
533 at_whole_second + Duration::from_nanos(tick)
534 ),
535 Some(Duration::ZERO)
536 );
537 assert_eq!(
538 parse_rate_limit_reset_at("2026-10-18T10:00:00-02:00", now),
539 Some(Duration::from_secs(90 + 7200))
540 );
541 assert_eq!(parse_rate_limit_reset_at("1000", now), Some(Duration::ZERO));
542 assert_eq!(parse_rate_limit_reset_at("2026-13-18T10:00:00Z", now), None);
543 assert_eq!(parse_rate_limit_reset_at("2026-10-18T10:00:00", now), None);
544 assert_eq!(parse_rate_limit_reset_at("soon", now), None);
545 assert_eq!(parse_rate_limit_reset_at("", now), None);
546 }
547}