leyline-http 0.1.5

HTTP client that impersonates Chrome, Firefox, and Safari on the wire: TLS (JA3/JA4), HTTP/2, and HTTP/3 fingerprints
Documentation
use super::wait::unix_from_ymd_hms;
use super::*;

#[test]
fn default_policy_retries_5xx_subset() {
    let p = RetryPolicy::transient();
    assert!(p.matches_status(429));
    assert!(p.matches_status(502));
    assert!(p.matches_status(503));
    assert!(p.matches_status(504));
    assert!(!p.matches_status(500));
    assert!(!p.matches_status(400));
}

#[test]
fn server_error_trigger_matches_all_5xx() {
    let p = RetryPolicy {
        retry_on: vec![RetryTrigger::ServerError],
        ..RetryPolicy::transient()
    };
    assert!(p.matches_status(500));
    assert!(p.matches_status(599));
    assert!(!p.matches_status(400));
}

#[test]
fn backoff_grows_and_caps() {
    let p = RetryPolicy {
        max_retries: 10,
        initial_backoff: Duration::from_millis(100),
        max_backoff: Duration::from_millis(800),
        max_retry_after: Duration::ZERO,
        backoff_factor: 2.0,
        jitter: false,
        retry_on: vec![RetryTrigger::ConnectionError],
        allow_non_idempotent: false,
        proxies: Vec::new(),
        ..RetryPolicy::transient()
    };
    assert!(p.backoff(0).as_millis() <= 100);
    assert!(p.backoff(1).as_millis() <= 200);
    assert!(p.backoff(10).as_millis() <= 800);
}

#[test]
fn none_is_no_retry() {
    let p = RetryPolicy::none();
    assert!(p.is_none());
    assert_eq!(p.max_retries, 0);
}

#[test]
fn retry_after_parses_delta_seconds_only() {
    assert_eq!(parse_retry_after("120"), Some(Duration::from_secs(120)));
    assert_eq!(parse_retry_after("  5 "), Some(Duration::from_secs(5)));
    assert_eq!(parse_retry_after("0"), Some(Duration::ZERO));
    assert_eq!(parse_retry_after("soon"), None);
}

#[test]
fn retry_after_parses_past_imf_fixdate_as_zero() {
    assert_eq!(
        parse_retry_after("Wed, 21 Oct 2025 07:28:00 GMT"),
        Some(Duration::ZERO)
    );
}

#[test]
fn imf_fixdate_epoch() {
    assert_eq!(
        unix_from_ymd_hms(1970, 1, 1, 0, 0, 0),
        Some(std::time::UNIX_EPOCH)
    );
}

#[test]
fn a_retry_after_date_past_the_platform_clock_does_not_panic() {
    let wait = parse_retry_after("Fri, 01 Jan 99999 00:00:00 GMT");
    assert!(wait.is_none_or(|wait| wait > Duration::from_secs(1 << 40)));
}

#[test]
fn wait_formats_parse_header_values() {
    assert_eq!(WaitFormat::Seconds.parse("7"), Some(Duration::from_secs(7)));
    let soon = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs()
        + 60;
    let wait = WaitFormat::UnixSeconds.parse(&soon.to_string()).unwrap();
    assert!(wait > Duration::from_secs(50) && wait <= Duration::from_secs(60));
    assert_eq!(WaitFormat::Seconds.parse("soon"), None);
}

#[test]
fn a_failed_tls_handshake_is_unsent() {
    let policy = RetryPolicy::transient().retry_unsent(true);
    let handshake: Result<Response> =
        Err(Error::from(crate::tls::TlsError::Handshake("alert".into())));
    assert!(policy.retries_unsent(&handshake));
}