use std::time::Duration;
#[cfg(not(target_arch = "wasm32"))]
use std::time::{SystemTime, UNIX_EPOCH};
pub fn should_retry(status: u16, attempt: u32, max_retries: u32, retry_after: Option<Duration>) -> Option<Duration> {
if attempt >= max_retries {
return None;
}
if !matches!(status, 429 | 500 | 502 | 503 | 504 | 529) {
return None;
}
if matches!(status, 429 | 529)
&& let Some(server_delay) = retry_after
{
return Some(server_delay.min(Duration::from_secs(60)));
}
Some(exponential_backoff(attempt))
}
pub fn should_retry_transport_error(attempt: u32, max_retries: u32) -> Option<Duration> {
if attempt >= max_retries {
return None;
}
Some(exponential_backoff(attempt))
}
fn exponential_backoff(attempt: u32) -> Duration {
let base_delay = Duration::from_secs(1u64.checked_shl(attempt).unwrap_or(u64::MAX));
let capped = base_delay.min(Duration::from_secs(30));
jittered(capped)
}
#[cfg(not(target_arch = "wasm32"))]
fn jittered(delay: Duration) -> Duration {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos();
let jitter_factor = 0.5 + (f64::from(nanos % 1000) / 2000.0);
delay.mul_f64(jitter_factor)
}
#[cfg(target_arch = "wasm32")]
fn jittered(delay: Duration) -> Duration {
delay
}
pub fn parse_retry_after(value: &str) -> Option<Duration> {
let trimmed = value.trim();
if let Ok(secs) = trimmed.parse::<u64>() {
return Some(Duration::from_secs(secs));
}
tracing::warn!(
retry_after = trimmed,
"Retry-After header uses HTTP-date format which is not yet supported; \
falling back to exponential backoff"
);
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retry_on_529_overloaded() {
assert!(should_retry(529, 0, 3, None).is_some());
assert!(should_retry(529, 2, 3, None).is_some());
assert!(
should_retry(529, 3, 3, None).is_none(),
"must still respect max_retries"
);
}
#[test]
fn retry_after_header_respected_on_529() {
let server_delay = Duration::from_secs(17);
let delay = should_retry(529, 0, 3, Some(server_delay)).expect("should retry on 529 with Retry-After");
assert_eq!(delay, server_delay);
}
#[test]
fn retry_after_still_capped_at_60s_on_529() {
let server_delay = Duration::from_secs(999);
let delay = should_retry(529, 0, 3, Some(server_delay)).expect("should retry on 529 with Retry-After");
assert_eq!(delay, Duration::from_secs(60));
}
#[test]
fn retry_after_still_ignored_on_500() {
let server_delay = Duration::from_secs(42);
let delay = should_retry(500, 0, 3, Some(server_delay)).expect("should retry on 500");
assert!(
delay < server_delay,
"500 must fall back to exponential backoff, not the server delay"
);
}
#[test]
fn should_retry_transport_error_respects_max_retries() {
assert!(should_retry_transport_error(0, 3).is_some());
assert!(should_retry_transport_error(2, 3).is_some());
assert!(should_retry_transport_error(3, 3).is_none());
}
#[test]
fn should_retry_transport_error_none_when_retries_disabled() {
assert!(should_retry_transport_error(0, 0).is_none());
}
}