use std::time::Duration;
use http::{HeaderMap, StatusCode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Disposition {
Success,
AuthDead,
RateLimited { retry_after: Option<Duration> },
Transient,
Permanent,
}
impl Disposition {
pub fn is_success(&self) -> bool {
matches!(self, Self::Success)
}
pub fn should_failover(&self) -> bool {
matches!(
self,
Self::AuthDead | Self::RateLimited { .. } | Self::Transient
)
}
pub fn from_http(status: StatusCode, headers: &HeaderMap) -> Self {
let code = status.as_u16();
match code {
200..=299 => Self::Success,
401..=403 => Self::AuthDead,
429 => Self::RateLimited {
retry_after: parse_retry_after(headers),
},
500..=599 => Self::Transient,
_ => Self::Permanent,
}
}
}
fn parse_retry_after(headers: &HeaderMap) -> Option<Duration> {
let val = headers
.get(http::header::RETRY_AFTER)?
.to_str()
.ok()?
.trim();
if let Ok(secs) = val.parse::<u64>() {
return Some(Duration::from_secs(secs));
}
let target = httpdate::parse_http_date(val)
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_secs() as i64;
let now = unix_now();
(target > now).then(|| Duration::from_secs((target - now) as u64))
}
#[cfg(not(target_arch = "wasm32"))]
fn unix_now() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64
}
#[cfg(target_arch = "wasm32")]
fn unix_now() -> i64 {
(js_sys::Date::now() / 1000.0) as i64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_mapping() {
let h = HeaderMap::new();
assert_eq!(
Disposition::from_http(StatusCode::OK, &h),
Disposition::Success
);
assert_eq!(
Disposition::from_http(StatusCode::UNAUTHORIZED, &h),
Disposition::AuthDead
);
assert_eq!(
Disposition::from_http(StatusCode::BAD_GATEWAY, &h),
Disposition::Transient
);
assert_eq!(
Disposition::from_http(StatusCode::BAD_REQUEST, &h),
Disposition::Permanent
);
assert!(Disposition::from_http(StatusCode::OK, &h).is_success());
}
#[test]
fn retry_after_parsed() {
let mut h = HeaderMap::new();
h.insert(http::header::RETRY_AFTER, "12".parse().unwrap());
assert_eq!(
Disposition::from_http(StatusCode::TOO_MANY_REQUESTS, &h),
Disposition::RateLimited {
retry_after: Some(Duration::from_secs(12))
}
);
}
#[test]
fn retry_after_http_date_form() {
let mut h = HeaderMap::new();
h.insert(
http::header::RETRY_AFTER,
"Wed, 21 Oct 2099 07:28:00 GMT".parse().unwrap(),
);
match Disposition::from_http(StatusCode::TOO_MANY_REQUESTS, &h) {
Disposition::RateLimited {
retry_after: Some(d),
} => assert!(d.as_secs() > 0, "future date → positive delay"),
other => panic!("expected RateLimited with delay, got {other:?}"),
}
let mut past = HeaderMap::new();
past.insert(
http::header::RETRY_AFTER,
"Wed, 21 Oct 1999 07:28:00 GMT".parse().unwrap(),
);
assert_eq!(
Disposition::from_http(StatusCode::TOO_MANY_REQUESTS, &past),
Disposition::RateLimited { retry_after: None },
"past date → no cooldown"
);
}
}