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> {
191 let v = value.trim();
192 let at = if !v.is_empty() && v.bytes().all(|b| b.is_ascii_digit()) {
193 let secs: u64 = v.parse().ok()?;
194 UNIX_EPOCH.checked_add(Duration::from_secs(secs))?
195 } else {
196 parse_iso8601_instant(v)?
197 };
198 Some(at.duration_since(now).unwrap_or(Duration::ZERO))
199}
200
201pub fn parse_rate_limit_reset(value: &str) -> Option<Duration> {
203 parse_rate_limit_reset_at(value, SystemTime::now())
204}
205
206fn parse_iso8601_instant(v: &str) -> Option<SystemTime> {
208 let b = v.as_bytes();
209 if b.len() < 20 {
210 return None;
211 }
212 let num = |s: Option<&str>| -> Option<i64> {
213 let s = s?;
214 if !s.is_empty() && s.bytes().all(|c| c.is_ascii_digit()) {
215 s.parse().ok()
216 } else {
217 None
218 }
219 };
220 if b[4] != b'-' || b[7] != b'-' || !matches!(b[10], b'T' | b't' | b' ') {
221 return None;
222 }
223 if b[13] != b':' || b[16] != b':' {
224 return None;
225 }
226 let year = num(v.get(0..4))?;
227 let month = num(v.get(5..7))?;
228 let day = num(v.get(8..10))?;
229 let hour = num(v.get(11..13))?;
230 let minute = num(v.get(14..16))?;
231 let second = num(v.get(17..19))?;
232 if !(1..=12).contains(&month)
233 || !(1..=31).contains(&day)
234 || hour > 23
235 || minute > 59
236 || second > 60
237 {
238 return None;
239 }
240 let mut i = 19;
241 if b.get(i) == Some(&b'.') {
242 i += 1;
243 let start = i;
244 while b.get(i).is_some_and(|c| c.is_ascii_digit()) {
245 i += 1;
246 }
247 if i == start {
248 return None;
249 }
250 }
251 let offset: i64 = match *b.get(i)? {
252 b'Z' | b'z' => {
253 i += 1;
254 0
255 }
256 sign @ (b'+' | b'-') => {
257 if b.get(i + 3) != Some(&b':') {
258 return None;
259 }
260 let oh = num(v.get(i + 1..i + 3))?;
261 let om = num(v.get(i + 4..i + 6))?;
262 i += 6;
263 let secs = oh * 3600 + om * 60;
264 if sign == b'-' {
265 -secs
266 } else {
267 secs
268 }
269 }
270 _ => return None,
271 };
272 if i != b.len() {
273 return None;
274 }
275 let days = days_from_civil(year, month, day);
276 let secs = days * 86_400 + hour * 3_600 + minute * 60 + second - offset;
277 if secs < 0 {
278 return None;
279 }
280 Some(UNIX_EPOCH + Duration::from_secs(secs as u64))
281}
282
283fn month_number(m: &str) -> Option<i64> {
284 const MONTHS: [&str; 12] = [
285 "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec",
286 ];
287 let m = m.get(..3)?.to_ascii_lowercase();
288 MONTHS.iter().position(|x| *x == m).map(|i| i as i64 + 1)
289}
290
291fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
292 let y = if m <= 2 { y - 1 } else { y };
293 let era = y.div_euclid(400);
294 let yoe = y - era * 400;
295 let mp = (m + 9) % 12;
296 let doy = (153 * mp + 2) / 5 + d - 1;
297 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
298 era * 146_097 + doe - 719_468
299}
300
301pub(crate) fn parse_http_date(v: &str) -> Option<SystemTime> {
303 let t: Vec<&str> = v.split_whitespace().collect();
304 let (day, mon, year, time) = match t.len() {
305 6 if t[0].ends_with(',') && t[5].eq_ignore_ascii_case("GMT") => {
306 (t[1], t[2], t[3].parse::<i64>().ok()?, t[4])
307 }
308 4 if t[0].ends_with(',') && t[3].eq_ignore_ascii_case("GMT") => {
309 let mut p = t[1].split('-');
310 let (d, m, y) = (p.next()?, p.next()?, p.next()?);
311 if p.next().is_some() {
312 return None;
313 }
314 let yy = y.parse::<i64>().ok()?;
315 let year = if y.len() == 2 {
316 if yy < 70 {
317 2000 + yy
318 } else {
319 1900 + yy
320 }
321 } else {
322 yy
323 };
324 (d, m, year, t[2])
325 }
326 5 => (t[2], t[1], t[4].parse::<i64>().ok()?, t[3]),
327 _ => return None,
328 };
329 let day = day.parse::<i64>().ok()?;
330 let month = month_number(mon)?;
331 let mut hms = time.split(':');
332 let h = hms.next()?.parse::<i64>().ok()?;
333 let mi = hms.next()?.parse::<i64>().ok()?;
334 let s = hms.next()?.parse::<i64>().ok()?;
335 if hms.next().is_some()
336 || !(1..=31).contains(&day)
337 || !(0..24).contains(&h)
338 || !(0..60).contains(&mi)
339 || !(0..=60).contains(&s)
340 || !(1970..=9999).contains(&year)
341 {
342 return None;
343 }
344 let secs = days_from_civil(year, month, day) * 86_400 + h * 3600 + mi * 60 + s;
345 u64::try_from(secs)
346 .ok()
347 .map(|s| UNIX_EPOCH + Duration::from_secs(s))
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn huge_retry_after_saturates_instead_of_panicking() {
356 for v in [
357 "18446744073709551616",
358 "1e300",
359 "99999999999999999999999999999999999999",
360 "1.7976931348623157e308",
361 ] {
362 assert_eq!(parse_retry_after(v), Some(Duration::MAX), "{v}");
363 }
364 assert_eq!(parse_retry_after("1.5"), Some(Duration::from_millis(1500)));
365 assert_eq!(parse_retry_after("-1"), None);
366 assert_eq!(parse_retry_after("NaN"), None);
367 assert_eq!(parse_retry_after("inf"), None);
368 }
369
370 #[test]
371 fn delay_caps_a_saturated_retry_after() {
372 let p = RetryPolicy {
373 max_retry_after: Duration::from_secs(30),
374 jitter: 0.0,
375 ..RetryPolicy::default()
376 };
377 assert_eq!(p.delay(1, Some(Duration::MAX)), Duration::from_secs(30));
378 }
379
380 #[test]
381 fn backoff_with_absurd_settings_does_not_panic() {
382 let p = RetryPolicy {
383 initial_backoff: Duration::MAX,
384 max_backoff: Duration::MAX,
385 multiplier: f64::MAX,
386 jitter: 0.0,
387 ..RetryPolicy::default()
388 };
389 let _ = p.backoff(40);
390 }
391
392 #[test]
393 fn retry_after_http_date_forms() {
394 let at = UNIX_EPOCH + Duration::from_secs(784_111_777); let now = at - Duration::from_secs(90);
396 for v in [
397 "Sun, 06 Nov 1994 08:49:37 GMT",
398 "Sunday, 06-Nov-94 08:49:37 GMT",
399 "Sun Nov 6 08:49:37 1994",
400 ] {
401 assert_eq!(
402 parse_retry_after_at(v, now),
403 Some(Duration::from_secs(90)),
404 "{v}"
405 );
406 }
407 assert_eq!(
409 parse_retry_after_at("Sun, 06 Nov 1994 08:49:37 GMT", at + Duration::from_secs(5)),
410 Some(Duration::ZERO)
411 );
412 assert_eq!(parse_retry_after_at("soon", now), None);
413 assert_eq!(
414 parse_retry_after_at("Sun, 32 Nov 1994 08:49:37 GMT", now),
415 None
416 );
417 }
418
419 #[test]
420 fn retry_after_beyond_the_cap_is_kept_whole_and_defers_on_request() {
421 assert_eq!(parse_retry_after("3600"), Some(Duration::from_secs(3600)));
422 let mut p = RetryPolicy::default();
423 assert!(!p.defers(Some(Duration::from_secs(3600))));
424 p.defer_over_cap = true;
425 assert!(p.defers(Some(Duration::from_secs(3600))));
426 assert!(!p.defers(Some(Duration::from_secs(10))));
427 assert!(!p.defers(None));
428 p.honor_retry_after = false;
429 assert!(!p.defers(Some(Duration::from_secs(3600))));
430 }
431
432 #[test]
433 fn separate_budgets_do_not_steal_from_each_other_and_stay_finite() {
434 let policy = RetryPolicy {
435 max_attempts: 100,
436 max_transport_attempts: Some(2),
437 max_status_attempts: Some(3),
438 ..RetryPolicy::default()
439 };
440 let mut b = AttemptBudgets::new();
441 assert_eq!(b.transport_failed(&policy, 1), Some(1));
443 assert_eq!(b.transport_failed(&policy, 2), None);
444 assert_eq!(b.status_failed(&policy, 3), Some(1));
446 assert_eq!(b.status_failed(&policy, 4), Some(2));
447 assert_eq!(b.status_failed(&policy, 5), None);
448 assert_eq!(b.transport_failed(&policy, 6), None);
450 let capped = RetryPolicy {
452 max_attempts: 4,
453 max_transport_attempts: Some(10),
454 max_status_attempts: Some(10),
455 ..RetryPolicy::default()
456 };
457 let mut b = AttemptBudgets::new();
458 assert!(b.transport_failed(&capped, 1).is_some());
459 assert!(b.status_failed(&capped, 2).is_some());
460 assert!(b.transport_failed(&capped, 3).is_some());
461 assert_eq!(b.status_failed(&capped, 4), None);
462 let legacy = RetryPolicy {
464 max_attempts: 3,
465 ..RetryPolicy::default()
466 };
467 assert!(!legacy.separate_budgets());
468 let mut b = AttemptBudgets::new();
469 assert_eq!(b.transport_failed(&legacy, 1), Some(1));
470 assert_eq!(b.status_failed(&legacy, 2), Some(2));
471 assert_eq!(b.transport_failed(&legacy, 3), None);
472 }
473
474 #[test]
475 fn rate_limit_reset_epoch_and_iso_forms() {
476 let now = UNIX_EPOCH + Duration::from_secs(1_792_317_600 - 90);
478 let want = Some(Duration::from_secs(90));
479 assert_eq!(parse_rate_limit_reset_at("1792317600", now), want);
480 assert_eq!(parse_rate_limit_reset_at("2026-10-18T10:00:00Z", now), want);
481 assert_eq!(
482 parse_rate_limit_reset_at("2026-10-18T12:00:00+02:00", now),
483 want
484 );
485 assert_eq!(
486 parse_rate_limit_reset_at("2026-10-18 10:00:00.250Z", now),
487 Some(Duration::from_secs(90))
488 );
489 assert_eq!(
490 parse_rate_limit_reset_at("2026-10-18T10:00:00-02:00", now),
491 Some(Duration::from_secs(90 + 7200))
492 );
493 assert_eq!(parse_rate_limit_reset_at("1000", now), Some(Duration::ZERO));
494 assert_eq!(parse_rate_limit_reset_at("2026-13-18T10:00:00Z", now), None);
495 assert_eq!(parse_rate_limit_reset_at("2026-10-18T10:00:00", now), None);
496 assert_eq!(parse_rate_limit_reset_at("soon", now), None);
497 assert_eq!(parse_rate_limit_reset_at("", now), None);
498 }
499}