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}
23
24impl Default for RetryPolicy {
25 fn default() -> Self {
26 Self {
27 max_attempts: 3,
28 initial_backoff: Duration::from_millis(500),
29 max_backoff: Duration::from_secs(8),
30 multiplier: 2.0,
31 jitter: 0.2,
32 retry_statuses: vec![408, 425, 429, 500, 502, 503, 504],
33 honor_retry_after: true,
34 max_retry_after: Duration::from_secs(30),
35 defer_over_cap: false,
36 }
37 }
38}
39
40impl RetryPolicy {
41 pub fn defers(&self, retry_after: Option<Duration>) -> bool {
44 self.defer_over_cap
45 && self.honor_retry_after
46 && retry_after.is_some_and(|ra| ra > self.max_retry_after)
47 }
48
49 pub fn none() -> Self {
50 Self {
51 max_attempts: 1,
52 ..Self::default()
53 }
54 }
55
56 pub fn is_retryable_status(&self, status: u16) -> bool {
57 self.retry_statuses.contains(&status)
58 }
59
60 pub fn backoff(&self, retry: u32) -> Duration {
62 let exp = retry.saturating_sub(1).min(32) as i32;
63 let secs = self.initial_backoff.as_secs_f64() * self.multiplier.powi(exp);
64 let capped = secs.min(self.max_backoff.as_secs_f64());
65 let shaved = if self.jitter > 0.0 {
66 capped * (1.0 - self.jitter.clamp(0.0, 1.0) * random_unit())
67 } else {
68 capped
69 };
70 Duration::try_from_secs_f64(shaved.max(0.0)).unwrap_or(self.max_backoff)
71 }
72
73 pub fn delay(&self, retry: u32, retry_after: Option<Duration>) -> Duration {
75 let base = self.backoff(retry);
76 match (self.honor_retry_after, retry_after) {
77 (true, Some(ra)) => base.max(ra.min(self.max_retry_after)),
78 _ => base,
79 }
80 }
81}
82
83fn random_unit() -> f64 {
84 let mut b = [0u8; 4];
85 if getrandom::fill(&mut b).is_err() {
86 return 0.0;
87 }
88 u32::from_le_bytes(b) as f64 / u32::MAX as f64
89}
90
91pub fn parse_retry_after(value: &str) -> Option<Duration> {
94 parse_retry_after_at(value, SystemTime::now())
95}
96
97pub fn parse_retry_after_at(value: &str, now: SystemTime) -> Option<Duration> {
99 let v = value.trim();
100 if let Ok(s) = v.parse::<f64>() {
101 return Some(s)
102 .filter(|s| s.is_finite() && *s >= 0.0)
103 .map(|s| Duration::try_from_secs_f64(s).unwrap_or(Duration::MAX));
104 }
105 let at = parse_http_date(v)?;
106 Some(at.duration_since(now).unwrap_or(Duration::ZERO))
107}
108
109pub fn parse_rate_limit_reset_at(value: &str, now: SystemTime) -> Option<Duration> {
116 let v = value.trim();
117 let at = if !v.is_empty() && v.bytes().all(|b| b.is_ascii_digit()) {
118 let secs: u64 = v.parse().ok()?;
119 UNIX_EPOCH.checked_add(Duration::from_secs(secs))?
120 } else {
121 parse_iso8601_instant(v)?
122 };
123 Some(at.duration_since(now).unwrap_or(Duration::ZERO))
124}
125
126pub fn parse_rate_limit_reset(value: &str) -> Option<Duration> {
128 parse_rate_limit_reset_at(value, SystemTime::now())
129}
130
131fn parse_iso8601_instant(v: &str) -> Option<SystemTime> {
133 let b = v.as_bytes();
134 if b.len() < 20 {
135 return None;
136 }
137 let num = |s: Option<&str>| -> Option<i64> {
138 let s = s?;
139 if !s.is_empty() && s.bytes().all(|c| c.is_ascii_digit()) {
140 s.parse().ok()
141 } else {
142 None
143 }
144 };
145 if b[4] != b'-' || b[7] != b'-' || !matches!(b[10], b'T' | b't' | b' ') {
146 return None;
147 }
148 if b[13] != b':' || b[16] != b':' {
149 return None;
150 }
151 let year = num(v.get(0..4))?;
152 let month = num(v.get(5..7))?;
153 let day = num(v.get(8..10))?;
154 let hour = num(v.get(11..13))?;
155 let minute = num(v.get(14..16))?;
156 let second = num(v.get(17..19))?;
157 if !(1..=12).contains(&month)
158 || !(1..=31).contains(&day)
159 || hour > 23
160 || minute > 59
161 || second > 60
162 {
163 return None;
164 }
165 let mut i = 19;
166 if b.get(i) == Some(&b'.') {
167 i += 1;
168 let start = i;
169 while b.get(i).is_some_and(|c| c.is_ascii_digit()) {
170 i += 1;
171 }
172 if i == start {
173 return None;
174 }
175 }
176 let offset: i64 = match *b.get(i)? {
177 b'Z' | b'z' => {
178 i += 1;
179 0
180 }
181 sign @ (b'+' | b'-') => {
182 if b.get(i + 3) != Some(&b':') {
183 return None;
184 }
185 let oh = num(v.get(i + 1..i + 3))?;
186 let om = num(v.get(i + 4..i + 6))?;
187 i += 6;
188 let secs = oh * 3600 + om * 60;
189 if sign == b'-' {
190 -secs
191 } else {
192 secs
193 }
194 }
195 _ => return None,
196 };
197 if i != b.len() {
198 return None;
199 }
200 let days = days_from_civil(year, month, day);
201 let secs = days * 86_400 + hour * 3_600 + minute * 60 + second - offset;
202 if secs < 0 {
203 return None;
204 }
205 Some(UNIX_EPOCH + Duration::from_secs(secs as u64))
206}
207
208fn month_number(m: &str) -> Option<i64> {
209 const MONTHS: [&str; 12] = [
210 "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec",
211 ];
212 let m = m.get(..3)?.to_ascii_lowercase();
213 MONTHS.iter().position(|x| *x == m).map(|i| i as i64 + 1)
214}
215
216fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
217 let y = if m <= 2 { y - 1 } else { y };
218 let era = y.div_euclid(400);
219 let yoe = y - era * 400;
220 let mp = (m + 9) % 12;
221 let doy = (153 * mp + 2) / 5 + d - 1;
222 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
223 era * 146_097 + doe - 719_468
224}
225
226pub(crate) fn parse_http_date(v: &str) -> Option<SystemTime> {
228 let t: Vec<&str> = v.split_whitespace().collect();
229 let (day, mon, year, time) = match t.len() {
230 6 if t[0].ends_with(',') && t[5].eq_ignore_ascii_case("GMT") => {
231 (t[1], t[2], t[3].parse::<i64>().ok()?, t[4])
232 }
233 4 if t[0].ends_with(',') && t[3].eq_ignore_ascii_case("GMT") => {
234 let mut p = t[1].split('-');
235 let (d, m, y) = (p.next()?, p.next()?, p.next()?);
236 if p.next().is_some() {
237 return None;
238 }
239 let yy = y.parse::<i64>().ok()?;
240 let year = if y.len() == 2 {
241 if yy < 70 {
242 2000 + yy
243 } else {
244 1900 + yy
245 }
246 } else {
247 yy
248 };
249 (d, m, year, t[2])
250 }
251 5 => (t[2], t[1], t[4].parse::<i64>().ok()?, t[3]),
252 _ => return None,
253 };
254 let day = day.parse::<i64>().ok()?;
255 let month = month_number(mon)?;
256 let mut hms = time.split(':');
257 let h = hms.next()?.parse::<i64>().ok()?;
258 let mi = hms.next()?.parse::<i64>().ok()?;
259 let s = hms.next()?.parse::<i64>().ok()?;
260 if hms.next().is_some()
261 || !(1..=31).contains(&day)
262 || !(0..24).contains(&h)
263 || !(0..60).contains(&mi)
264 || !(0..=60).contains(&s)
265 || !(1970..=9999).contains(&year)
266 {
267 return None;
268 }
269 let secs = days_from_civil(year, month, day) * 86_400 + h * 3600 + mi * 60 + s;
270 u64::try_from(secs)
271 .ok()
272 .map(|s| UNIX_EPOCH + Duration::from_secs(s))
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn huge_retry_after_saturates_instead_of_panicking() {
281 for v in [
282 "18446744073709551616",
283 "1e300",
284 "99999999999999999999999999999999999999",
285 "1.7976931348623157e308",
286 ] {
287 assert_eq!(parse_retry_after(v), Some(Duration::MAX), "{v}");
288 }
289 assert_eq!(parse_retry_after("1.5"), Some(Duration::from_millis(1500)));
290 assert_eq!(parse_retry_after("-1"), None);
291 assert_eq!(parse_retry_after("NaN"), None);
292 assert_eq!(parse_retry_after("inf"), None);
293 }
294
295 #[test]
296 fn delay_caps_a_saturated_retry_after() {
297 let p = RetryPolicy {
298 max_retry_after: Duration::from_secs(30),
299 jitter: 0.0,
300 ..RetryPolicy::default()
301 };
302 assert_eq!(p.delay(1, Some(Duration::MAX)), Duration::from_secs(30));
303 }
304
305 #[test]
306 fn backoff_with_absurd_settings_does_not_panic() {
307 let p = RetryPolicy {
308 initial_backoff: Duration::MAX,
309 max_backoff: Duration::MAX,
310 multiplier: f64::MAX,
311 jitter: 0.0,
312 ..RetryPolicy::default()
313 };
314 let _ = p.backoff(40);
315 }
316
317 #[test]
318 fn retry_after_http_date_forms() {
319 let at = UNIX_EPOCH + Duration::from_secs(784_111_777); let now = at - Duration::from_secs(90);
321 for v in [
322 "Sun, 06 Nov 1994 08:49:37 GMT",
323 "Sunday, 06-Nov-94 08:49:37 GMT",
324 "Sun Nov 6 08:49:37 1994",
325 ] {
326 assert_eq!(
327 parse_retry_after_at(v, now),
328 Some(Duration::from_secs(90)),
329 "{v}"
330 );
331 }
332 assert_eq!(
334 parse_retry_after_at("Sun, 06 Nov 1994 08:49:37 GMT", at + Duration::from_secs(5)),
335 Some(Duration::ZERO)
336 );
337 assert_eq!(parse_retry_after_at("soon", now), None);
338 assert_eq!(
339 parse_retry_after_at("Sun, 32 Nov 1994 08:49:37 GMT", now),
340 None
341 );
342 }
343
344 #[test]
345 fn retry_after_beyond_the_cap_is_kept_whole_and_defers_on_request() {
346 assert_eq!(parse_retry_after("3600"), Some(Duration::from_secs(3600)));
347 let mut p = RetryPolicy::default();
348 assert!(!p.defers(Some(Duration::from_secs(3600))));
349 p.defer_over_cap = true;
350 assert!(p.defers(Some(Duration::from_secs(3600))));
351 assert!(!p.defers(Some(Duration::from_secs(10))));
352 assert!(!p.defers(None));
353 p.honor_retry_after = false;
354 assert!(!p.defers(Some(Duration::from_secs(3600))));
355 }
356
357 #[test]
358 fn rate_limit_reset_epoch_and_iso_forms() {
359 let now = UNIX_EPOCH + Duration::from_secs(1_792_317_600 - 90);
361 let want = Some(Duration::from_secs(90));
362 assert_eq!(parse_rate_limit_reset_at("1792317600", now), want);
363 assert_eq!(parse_rate_limit_reset_at("2026-10-18T10:00:00Z", now), want);
364 assert_eq!(
365 parse_rate_limit_reset_at("2026-10-18T12:00:00+02:00", now),
366 want
367 );
368 assert_eq!(
369 parse_rate_limit_reset_at("2026-10-18 10:00:00.250Z", now),
370 Some(Duration::from_secs(90))
371 );
372 assert_eq!(
373 parse_rate_limit_reset_at("2026-10-18T10:00:00-02:00", now),
374 Some(Duration::from_secs(90 + 7200))
375 );
376 assert_eq!(parse_rate_limit_reset_at("1000", now), Some(Duration::ZERO));
377 assert_eq!(parse_rate_limit_reset_at("2026-13-18T10:00:00Z", now), None);
378 assert_eq!(parse_rate_limit_reset_at("2026-10-18T10:00:00", now), None);
379 assert_eq!(parse_rate_limit_reset_at("soon", now), None);
380 assert_eq!(parse_rate_limit_reset_at("", now), None);
381 }
382}