use reqwest::header::HeaderMap;
use reqwest::StatusCode;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone)]
pub struct RetryPolicy {
pub max_retries: u32,
pub initial_backoff: Duration,
pub max_backoff: Duration,
pub multiplier: f64,
pub retryable_statuses: Vec<u16>,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_retries: 2,
initial_backoff: Duration::from_millis(500),
max_backoff: Duration::from_secs(8),
multiplier: 2.0,
retryable_statuses: vec![408, 409, 425, 429, 500, 502, 503, 504, 529],
}
}
}
impl RetryPolicy {
pub fn none() -> Self {
Self {
max_retries: 0,
..Self::default()
}
}
pub fn should_retry_status(&self, status: StatusCode) -> bool {
self.retryable_statuses.contains(&status.as_u16())
}
pub fn backoff_for(&self, attempt: u32) -> Duration {
let multiplier = if self.multiplier.is_finite() && self.multiplier > 0.0 {
self.multiplier
} else {
2.0
};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
let jitter = 1.0 + (nanos % 250) as f64 / 1000.0;
let secs =
self.initial_backoff.as_secs_f64() * multiplier.powi(attempt.min(64) as i32) * jitter;
let max_secs = self.max_backoff.as_secs_f64();
if !secs.is_finite() || secs < 0.0 || secs >= max_secs {
return self.max_backoff;
}
Duration::from_secs_f64(secs)
}
pub(crate) fn delay_for(&self, attempt: u32, headers: &HeaderMap) -> Duration {
let backoff = self.backoff_for(attempt);
match parse_retry_after(headers) {
Some(hint) => backoff.max(hint.min(MAX_RETRY_AFTER)),
None => backoff,
}
}
}
pub(crate) const MAX_RETRY_AFTER: Duration = Duration::from_secs(60);
fn parse_retry_after(headers: &HeaderMap) -> Option<Duration> {
let value = headers.get(reqwest::header::RETRY_AFTER)?.to_str().ok()?;
let secs: f64 = value.trim().parse().ok()?;
(secs.is_finite() && secs >= 0.0).then(|| Duration::from_secs_f64(secs))
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest::header::HeaderValue;
#[test]
fn defaults_retry_documented_statuses() {
let p = RetryPolicy::default();
assert!(p.should_retry_status(StatusCode::TOO_MANY_REQUESTS));
assert!(p.should_retry_status(StatusCode::from_u16(529).unwrap()));
assert!(!p.should_retry_status(StatusCode::BAD_REQUEST));
assert!(!p.should_retry_status(StatusCode::UNAUTHORIZED));
}
#[test]
fn backoff_grows_and_is_capped() {
let p = RetryPolicy::default();
let b0 = p.backoff_for(0);
let b1 = p.backoff_for(1);
assert!(b0 >= Duration::from_millis(500));
assert!(b1 >= b0);
let huge = p.backoff_for(30);
assert!(huge <= Duration::from_secs(8));
}
#[test]
fn backoff_never_panics_on_extreme_inputs() {
let mut p = RetryPolicy::default();
assert_eq!(p.backoff_for(u32::MAX), Duration::from_secs(8));
p.multiplier = f64::NAN;
assert!(p.backoff_for(5) <= Duration::from_secs(8));
p.multiplier = f64::INFINITY;
assert!(p.backoff_for(5) <= Duration::from_secs(8));
p.multiplier = -3.0;
assert!(p.backoff_for(5) <= Duration::from_secs(8));
p.max_backoff = Duration::ZERO;
assert_eq!(p.backoff_for(0), Duration::ZERO);
}
#[test]
fn retry_after_hint_is_capped() {
let p = RetryPolicy::default();
let mut headers = HeaderMap::new();
headers.insert(
reqwest::header::RETRY_AFTER,
HeaderValue::from_static("999999999"),
);
let delay = p.delay_for(0, &headers);
assert!(
delay <= MAX_RETRY_AFTER,
"delay {delay:?} exceeded the cap {MAX_RETRY_AFTER:?}"
);
}
#[test]
fn parses_retry_after_seconds() {
let mut headers = HeaderMap::new();
headers.insert(reqwest::header::RETRY_AFTER, HeaderValue::from_static("2"));
assert_eq!(parse_retry_after(&headers), Some(Duration::from_secs(2)));
headers.insert(
reqwest::header::RETRY_AFTER,
HeaderValue::from_static("0.25"),
);
assert_eq!(
parse_retry_after(&headers),
Some(Duration::from_millis(250))
);
headers.insert(
reqwest::header::RETRY_AFTER,
HeaderValue::from_static("not-a-number"),
);
assert_eq!(parse_retry_after(&headers), None);
}
}