1use reqwest::header::HeaderMap;
4use reqwest::StatusCode;
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6
7#[derive(Debug, Clone)]
16pub struct RetryPolicy {
17 pub max_retries: u32,
20 pub initial_backoff: Duration,
22 pub max_backoff: Duration,
24 pub multiplier: f64,
26 pub retryable_statuses: Vec<u16>,
28}
29
30impl Default for RetryPolicy {
31 fn default() -> Self {
32 Self {
33 max_retries: 2,
34 initial_backoff: Duration::from_millis(500),
35 max_backoff: Duration::from_secs(8),
36 multiplier: 2.0,
37 retryable_statuses: vec![408, 409, 425, 429, 500, 502, 503, 504, 529],
38 }
39 }
40}
41
42impl RetryPolicy {
43 pub fn none() -> Self {
45 Self {
46 max_retries: 0,
47 ..Self::default()
48 }
49 }
50
51 pub fn should_retry_status(&self, status: StatusCode) -> bool {
53 self.retryable_statuses.contains(&status.as_u16())
54 }
55
56 pub fn backoff_for(&self, attempt: u32) -> Duration {
63 let multiplier = if self.multiplier.is_finite() && self.multiplier > 0.0 {
64 self.multiplier
65 } else {
66 2.0
67 };
68 let nanos = SystemTime::now()
70 .duration_since(UNIX_EPOCH)
71 .map(|d| d.subsec_nanos())
72 .unwrap_or(0);
73 let jitter = 1.0 + (nanos % 250) as f64 / 1000.0; let secs =
78 self.initial_backoff.as_secs_f64() * multiplier.powi(attempt.min(64) as i32) * jitter;
79
80 let max_secs = self.max_backoff.as_secs_f64();
81 if !secs.is_finite() || secs < 0.0 || secs >= max_secs {
82 return self.max_backoff;
85 }
86 Duration::from_secs_f64(secs)
87 }
88
89 pub(crate) fn delay_for(&self, attempt: u32, headers: &HeaderMap) -> Duration {
94 let backoff = self.backoff_for(attempt);
95 match parse_retry_after(headers) {
96 Some(hint) => backoff.max(hint.min(MAX_RETRY_AFTER)),
97 None => backoff,
98 }
99 }
100}
101
102pub(crate) const MAX_RETRY_AFTER: Duration = Duration::from_secs(60);
104
105fn parse_retry_after(headers: &HeaderMap) -> Option<Duration> {
107 let value = headers.get(reqwest::header::RETRY_AFTER)?.to_str().ok()?;
108 let secs: f64 = value.trim().parse().ok()?;
109 (secs.is_finite() && secs >= 0.0).then(|| Duration::from_secs_f64(secs))
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use reqwest::header::HeaderValue;
116
117 #[test]
118 fn defaults_retry_documented_statuses() {
119 let p = RetryPolicy::default();
120 assert!(p.should_retry_status(StatusCode::TOO_MANY_REQUESTS));
121 assert!(p.should_retry_status(StatusCode::from_u16(529).unwrap()));
122 assert!(!p.should_retry_status(StatusCode::BAD_REQUEST));
123 assert!(!p.should_retry_status(StatusCode::UNAUTHORIZED));
124 }
125
126 #[test]
127 fn backoff_grows_and_is_capped() {
128 let p = RetryPolicy::default();
129 let b0 = p.backoff_for(0);
130 let b1 = p.backoff_for(1);
131 assert!(b0 >= Duration::from_millis(500));
132 assert!(b1 >= b0);
133 let huge = p.backoff_for(30);
134 assert!(huge <= Duration::from_secs(8));
135 }
136
137 #[test]
138 fn backoff_never_panics_on_extreme_inputs() {
139 let mut p = RetryPolicy::default();
140
141 assert_eq!(p.backoff_for(u32::MAX), Duration::from_secs(8));
143
144 p.multiplier = f64::NAN;
146 assert!(p.backoff_for(5) <= Duration::from_secs(8));
147 p.multiplier = f64::INFINITY;
148 assert!(p.backoff_for(5) <= Duration::from_secs(8));
149 p.multiplier = -3.0;
150 assert!(p.backoff_for(5) <= Duration::from_secs(8));
151
152 p.max_backoff = Duration::ZERO;
154 assert_eq!(p.backoff_for(0), Duration::ZERO);
155 }
156
157 #[test]
158 fn retry_after_hint_is_capped() {
159 let p = RetryPolicy::default();
160 let mut headers = HeaderMap::new();
161 headers.insert(
163 reqwest::header::RETRY_AFTER,
164 HeaderValue::from_static("999999999"),
165 );
166 let delay = p.delay_for(0, &headers);
167 assert!(
168 delay <= MAX_RETRY_AFTER,
169 "delay {delay:?} exceeded the cap {MAX_RETRY_AFTER:?}"
170 );
171 }
172
173 #[test]
174 fn parses_retry_after_seconds() {
175 let mut headers = HeaderMap::new();
176 headers.insert(reqwest::header::RETRY_AFTER, HeaderValue::from_static("2"));
177 assert_eq!(parse_retry_after(&headers), Some(Duration::from_secs(2)));
178
179 headers.insert(
180 reqwest::header::RETRY_AFTER,
181 HeaderValue::from_static("0.25"),
182 );
183 assert_eq!(
184 parse_retry_after(&headers),
185 Some(Duration::from_millis(250))
186 );
187
188 headers.insert(
189 reqwest::header::RETRY_AFTER,
190 HeaderValue::from_static("not-a-number"),
191 );
192 assert_eq!(parse_retry_after(&headers), None);
193 }
194}