use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[must_use]
pub fn parse_retry_after(
retry_after: Option<&str>,
retry_after_ms: Option<&str>,
now: SystemTime,
) -> Option<Duration> {
if let Some(ms) = retry_after_ms.and_then(parse_seconds_like) {
return Duration::try_from_secs_f64(ms / 1000.0).ok();
}
let value = retry_after?.trim();
if let Some(secs) = parse_seconds_like(value) {
return Duration::try_from_secs_f64(secs).ok();
}
let at = parse_http_date(value, now)?;
Some(at.duration_since(now).unwrap_or(Duration::ZERO))
}
fn parse_seconds_like(value: &str) -> Option<f64> {
value
.trim()
.parse::<f64>()
.ok()
.filter(|v| v.is_finite() && *v >= 0.0)
}
fn parse_http_date(value: &str, now: SystemTime) -> Option<SystemTime> {
let parts: Vec<&str> = value.split_whitespace().collect();
let (day, month, year, time) = match parts.as_slice() {
[weekday, day, month, year, time, "GMT"] if weekday.ends_with(',') => {
(*day, *month, four_digit_year(year)?, *time)
}
[weekday, date, time, "GMT"] if weekday.ends_with(',') => {
let mut fields = date.split('-');
let (day, month, year) = (fields.next()?, fields.next()?, fields.next()?);
if fields.next().is_some() {
return None;
}
(day, month, two_digit_year(year, now)?, *time)
}
[_weekday, month, day, time, year] => (*day, *month, four_digit_year(year)?, *time),
_ => return None,
};
let day: u64 = day.parse().ok()?;
let month = month_number(month)?;
let mut hms = time.split(':').map(|p| p.parse::<u64>().ok());
let (h, m, s) = (hms.next()??, hms.next()??, hms.next()??);
if hms.next().is_some() || h > 23 || m > 59 || s > 60 || !(1..=31).contains(&day) {
return None;
}
let days = days_from_civil(year, month, day);
let secs = u64::try_from(days)
.ok()?
.checked_mul(86_400)?
.checked_add(h * 3600 + m * 60 + s)?;
UNIX_EPOCH.checked_add(Duration::from_secs(secs))
}
fn four_digit_year(value: &str) -> Option<i64> {
(value.len() == 4 && value.bytes().all(|b| b.is_ascii_digit()))
.then(|| value.parse().ok())
.flatten()
}
fn two_digit_year(value: &str, now: SystemTime) -> Option<i64> {
if value.len() != 2 || !value.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let yy: i64 = value.parse().ok()?;
let days_now = now.duration_since(UNIX_EPOCH).ok()?.as_secs() / 86_400;
let current = year_from_days(i64::try_from(days_now).ok()?);
let mut year = current - current.rem_euclid(100) + yy;
if year > current + 50 {
year -= 100;
}
Some(year)
}
fn month_number(name: &str) -> Option<i64> {
Some(match name {
"Jan" => 1,
"Feb" => 2,
"Mar" => 3,
"Apr" => 4,
"May" => 5,
"Jun" => 6,
"Jul" => 7,
"Aug" => 8,
"Sep" => 9,
"Oct" => 10,
"Nov" => 11,
"Dec" => 12,
_ => return None,
})
}
fn days_from_civil(year: i64, month: i64, day: u64) -> i64 {
let (y, mo) = if month <= 2 {
(year - 1, month + 9)
} else {
(year, month - 3)
};
let era = y.div_euclid(400);
let yoe = y - era * 400;
let doy = (153 * mo + 2) / 5 + day as i64 - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
fn year_from_days(days: i64) -> i64 {
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let year = yoe + era * 400;
if mp >= 10 { year + 1 } else { year }
}
#[cfg(test)]
mod tests {
use super::*;
fn now() -> SystemTime {
UNIX_EPOCH + Duration::from_secs(784_111_777)
}
#[test]
fn parses_delta_seconds_milliseconds_and_dates() {
let now = now();
assert_eq!(
parse_retry_after(Some("3"), None, now),
Some(Duration::from_secs(3))
);
assert_eq!(
parse_retry_after(Some("1.5"), None, now),
Some(Duration::from_millis(1500))
);
assert_eq!(
parse_retry_after(Some("3"), Some("250"), now),
Some(Duration::from_millis(250))
);
assert_eq!(
parse_retry_after(Some("Sun, 06 Nov 1994 08:49:47 GMT"), None, now),
Some(Duration::from_secs(10))
);
assert_eq!(
parse_retry_after(Some("Sun, 06 Nov 1994 08:49:27 GMT"), None, now),
Some(Duration::ZERO)
);
assert_eq!(
parse_retry_after(Some("soon"), None, SystemTime::now()),
None
);
assert_eq!(parse_retry_after(None, None, now), None);
}
#[test]
fn parses_the_obsolete_date_forms() {
let now = now();
assert_eq!(
parse_retry_after(Some("Sunday, 06-Nov-94 08:49:47 GMT"), None, now),
Some(Duration::from_secs(10))
);
assert_eq!(
parse_retry_after(Some("Sun Nov 6 08:49:47 1994"), None, now),
Some(Duration::from_secs(10))
);
let new_year_2026 =
UNIX_EPOCH + Duration::from_secs(days_from_civil(2026, 1, 1) as u64 * 86_400);
assert_eq!(
parse_retry_after(
Some("Thursday, 01-Jan-26 00:00:10 GMT"),
None,
new_year_2026
),
Some(Duration::from_secs(10))
);
assert_eq!(
parse_retry_after(Some("Sunday, 06-Nov-94 08:49:47 GMT"), None, new_year_2026),
Some(Duration::ZERO)
);
}
#[test]
fn huge_or_malformed_values_never_panic() {
let now = now();
for (retry_after, retry_after_ms) in [
(Some("1e20"), None),
(Some("99999999999999999999"), None),
(None, Some("1e30")),
(Some("1.9e19"), None),
(Some("Sun, 06 Nov 300000000000 08:49:37 GMT"), None),
(Some("Sun, 06 Nov 9223372036854775807 08:49:37 GMT"), None),
(Some("Sun, 06 Nov -9223372036854775807 08:49:37 GMT"), None),
(Some("Sun Nov 6 08:49:37 99999999999999"), None),
(
Some("Sunday, 06-Nov-9223372036854775807 08:49:37 GMT"),
None,
),
(Some("Sun, 99 Nov 1994 99:99:99 GMT"), None),
(Some("Sun, 06 Nov 1994 08:49:37:00 GMT"), None),
(Some("-1"), Some("-5")),
(Some("NaN"), Some("inf")),
] {
assert_eq!(
parse_retry_after(retry_after, retry_after_ms, now),
None,
"{retry_after:?} / {retry_after_ms:?}"
);
}
assert_eq!(
parse_retry_after(Some("Sunday, 06-Nov-94 08:49:47 GMT"), None, UNIX_EPOCH),
Some(Duration::from_secs(784_111_787))
);
}
#[test]
fn year_from_days_inverts_days_from_civil() {
for year in [1970, 1994, 2000, 2024, 2100, 9999] {
for (month, day) in [(1, 1), (2, 29), (12, 31)] {
let days = days_from_civil(year, month, day);
assert_eq!(year_from_days(days), year, "{year}-{month}-{day}");
}
}
}
}