use std::time::{Duration, SystemTime, UNIX_EPOCH};
use reqwest::header::HeaderMap;
pub(crate) fn equal_jitter_backoff(attempt: u32) -> Duration {
const BASE_MS: u64 = 500;
const CAP_MS: u64 = 30_000;
let exp = attempt.saturating_sub(1).min(20);
let shifted = BASE_MS.saturating_mul(1u64 << exp);
let jitter = pseudo_jitter_ms(BASE_MS);
Duration::from_millis(shifted.saturating_add(jitter).min(CAP_MS))
}
fn pseudo_jitter_ms(bound: u64) -> u64 {
if bound == 0 {
return 0;
}
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.subsec_nanos() as u64)
.unwrap_or(0);
nanos % bound
}
pub(crate) fn parse_retry_after(headers: &HeaderMap, body: &str) -> Option<Duration> {
let header_hint = headers
.get(reqwest::header::RETRY_AFTER)
.and_then(|v| v.to_str().ok())
.and_then(|s| {
let trimmed = s.trim();
if let Ok(secs) = trimmed.parse::<u64>() {
return Some(Duration::from_secs(secs));
}
parse_http_date_delta(trimmed)
});
let body_hint = parse_body_retry_after(body);
body_hint.or(header_hint)
}
fn parse_body_retry_after(body: &str) -> Option<Duration> {
if body.is_empty() {
return None;
}
#[derive(serde::Deserialize)]
struct Body {
#[serde(default)]
retry_after_us: Option<u64>,
#[serde(default)]
error: Option<String>,
}
if let Ok(b) = serde_json::from_str::<Body>(body) {
if let Some(us) = b.retry_after_us {
return Some(Duration::from_micros(us));
}
if let Some(msg) = b.error {
if let Some(secs) = parse_retry_after_english(&msg) {
return Some(Duration::from_secs(secs));
}
}
}
parse_retry_after_english(body).map(Duration::from_secs)
}
fn parse_retry_after_english(msg: &str) -> Option<u64> {
let lower = msg.to_ascii_lowercase();
let after = lower.split("retry after").nth(1)?.trim();
let num: String = after.chars().take_while(|c| c.is_ascii_digit()).collect();
num.parse().ok()
}
fn parse_http_date_delta(s: &str) -> Option<Duration> {
if s.len() < 29 || !s.ends_with(" GMT") {
return None;
}
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.len() != 6 {
return None;
}
let day: u32 = parts[1].parse().ok()?;
let month = month_from_abbrev(parts[2])?;
let year: i32 = parts[3].parse().ok()?;
let time_parts: Vec<&str> = parts[4].split(':').collect();
if time_parts.len() != 3 {
return None;
}
let hour: u32 = time_parts[0].parse().ok()?;
let minute: u32 = time_parts[1].parse().ok()?;
let second: u32 = time_parts[2].parse().ok()?;
let target_unix = civil_to_unix_seconds(year, month, day, hour, minute, second)?;
let now_unix = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs() as i64;
let delta = target_unix - now_unix;
if delta <= 0 {
None
} else {
Some(Duration::from_secs(delta as u64))
}
}
fn month_from_abbrev(m: &str) -> Option<u32> {
match m {
"Jan" => Some(1),
"Feb" => Some(2),
"Mar" => Some(3),
"Apr" => Some(4),
"May" => Some(5),
"Jun" => Some(6),
"Jul" => Some(7),
"Aug" => Some(8),
"Sep" => Some(9),
"Oct" => Some(10),
"Nov" => Some(11),
"Dec" => Some(12),
_ => None,
}
}
fn civil_to_unix_seconds(
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
) -> Option<i64> {
if !(1..=12).contains(&month)
|| !(1..=31).contains(&day)
|| hour >= 24
|| minute >= 60
|| second >= 60
{
return None;
}
let y = if month <= 2 { year - 1 } else { year };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = (y - era * 400) as u32; let doy = (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + day - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; let days_since_epoch = era as i64 * 146097 + doe as i64 - 719_468;
Some(days_since_epoch * 86_400 + hour as i64 * 3600 + minute as i64 * 60 + second as i64)
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest::header::HeaderValue;
#[test]
fn header_seconds_parses() {
let mut h = HeaderMap::new();
h.insert("retry-after", HeaderValue::from_static("7"));
assert_eq!(parse_retry_after(&h, ""), Some(Duration::from_secs(7)));
}
#[test]
fn body_retry_after_us_wins_over_header() {
let mut h = HeaderMap::new();
h.insert("retry-after", HeaderValue::from_static("10"));
let body = r#"{"retry_after_us":2500000}"#;
assert_eq!(
parse_retry_after(&h, body),
Some(Duration::from_micros(2_500_000))
);
}
#[test]
fn body_english_fallback() {
let h = HeaderMap::new();
let body = r#"{"error":"rate limited — retry after 3s"}"#;
assert_eq!(parse_retry_after(&h, body), Some(Duration::from_secs(3)));
}
#[test]
fn no_hint_returns_none() {
let h = HeaderMap::new();
assert_eq!(parse_retry_after(&h, ""), None);
}
#[test]
fn equal_jitter_attempt_1_is_in_band() {
let d = equal_jitter_backoff(1);
assert!(d >= Duration::from_millis(500));
assert!(d < Duration::from_millis(1_000));
}
#[test]
fn equal_jitter_caps_at_30s() {
assert!(equal_jitter_backoff(30) <= Duration::from_secs(30));
}
#[test]
fn http_date_malformed_returns_none() {
assert_eq!(parse_http_date_delta("not a date"), None);
assert_eq!(parse_http_date_delta(""), None);
}
}