From 64e73b1cab4edfc542900492a727ae0d68265bbf Mon Sep 17 00:00:00 2001 From: teernisse Date: Sat, 14 Feb 2026 10:20:01 -0500 Subject: [PATCH] fix(graphql): handle past HTTP dates in retry-after header gracefully Extract parse_retry_after_value(header, now) as a pure function to enable deterministic testing of Retry-After header parsing. The previous implementation used let-chains with SystemTime::now() inline, which made it untestable and would panic on negative durations when the server clock was behind or the header contained a date in the past. Changes: - Extract parse_retry_after_value() taking an explicit `now` parameter - Handle past HTTP dates by returning 1 second instead of panicking on negative Duration (date.duration_since(now) returns Err for past dates) - Trim whitespace from header values before parsing - Add test for past HTTP date returning 1 second minimum - Add test for delta-seconds with surrounding whitespace Co-Authored-By: Claude Opus 4.6 --- src/gitlab/graphql.rs | 15 +++++++++++---- src/gitlab/graphql_tests.rs | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/gitlab/graphql.rs b/src/gitlab/graphql.rs index 332d722..980fc02 100644 --- a/src/gitlab/graphql.rs +++ b/src/gitlab/graphql.rs @@ -126,14 +126,21 @@ fn parse_retry_after(response: &reqwest::Response) -> u64 { None => return 60, }; + parse_retry_after_value(header, SystemTime::now()) +} + +fn parse_retry_after_value(header: &str, now: SystemTime) -> u64 { + let header = header.trim(); + if let Ok(secs) = header.parse::() { return secs.max(1); } - if let Ok(date) = httpdate::parse_http_date(header) - && let Ok(delta) = date.duration_since(SystemTime::now()) - { - return delta.as_secs().max(1); + if let Ok(date) = httpdate::parse_http_date(header) { + return match date.duration_since(now) { + Ok(delta) => delta.as_secs().max(1), + Err(_) => 1, + }; } 60 diff --git a/src/gitlab/graphql_tests.rs b/src/gitlab/graphql_tests.rs index 21e963f..89af48e 100644 --- a/src/gitlab/graphql_tests.rs +++ b/src/gitlab/graphql_tests.rs @@ -244,6 +244,21 @@ async fn test_retry_after_invalid_falls_back_to_60() { } } +#[test] +fn test_retry_after_http_date_in_past_returns_one_second() { + let now = SystemTime::now(); + let past = now - Duration::from_secs(120); + let date_str = httpdate::fmt_http_date(past); + + assert_eq!(parse_retry_after_value(&date_str, now), 1); +} + +#[test] +fn test_retry_after_delta_seconds_trims_whitespace() { + let now = SystemTime::now(); + assert_eq!(parse_retry_after_value(" 120 ", now), 120); +} + #[tokio::test] async fn test_graphql_network_error() { let client = GraphqlClient::new("http://127.0.0.1:1", "token");