Skip to content

fix: correct Retry-After date header unit mismatch in retry backoff#308

Open
MaxwellCalkin wants to merge 1 commit intoanthropics:mainfrom
MaxwellCalkin:fix/retry-after-date-unit-mismatch
Open

fix: correct Retry-After date header unit mismatch in retry backoff#308
MaxwellCalkin wants to merge 1 commit intoanthropics:mainfrom
MaxwellCalkin:fix/retry-after-date-unit-mismatch

Conversation

@MaxwellCalkin
Copy link

@MaxwellCalkin MaxwellCalkin commented Mar 8, 2026

Summary

  • Bug: When Retry-After header contains an HTTP-date (e.g., Wed, 21 Oct 2015 07:28:00 GMT), the retry backoff duration is ~1,000,000x shorter than the server requested.
  • Root cause: ChronoUnit.MILLIS.between() returns milliseconds, but the result flows into Duration.ofNanos(), which interprets it as nanoseconds.
  • Fix: Change ChronoUnit.MILLIS to ChronoUnit.NANOS so the date-based path produces nanoseconds consistently with the other two code paths (Retry-After-Ms and numeric Retry-After).

Before (bug)

A Retry-After: Wed, 09 Oct 2024 07:28:05 GMT header (5 seconds in the future) would compute:

ChronoUnit.MILLIS.between(now, retryAfter) = 5000  (milliseconds)
Duration.ofNanos(5000) = 5 microseconds  // Wrong! Should be 5 seconds

After (fix)

ChronoUnit.NANOS.between(now, retryAfter) = 5000000000  (nanoseconds)
Duration.ofNanos(5000000000) = 5 seconds  // Correct

The other two branches already convert correctly to nanoseconds:

  • Retry-After-Ms: toFloat() * TimeUnit.MILLISECONDS.toNanos(1) → nanoseconds
  • Retry-After (numeric): toFloat() * TimeUnit.SECONDS.toNanos(1) → nanoseconds

Test plan

  • Verified existing execute_withRetryAfterHeader test still passes (it uses a no-op sleeper, so it validates the flow but not the duration value)
  • A unit test that asserts the correct duration when parsing an HTTP-date Retry-After header would fully validate this fix

AI Disclosure

This PR was authored by Claude Opus 4.6 (Anthropic), an AI agent operated by Maxwell Calkin (@MaxwellCalkin).

In `getRetryBackoffDuration`, when the `Retry-After` header contains an
HTTP-date value, `ChronoUnit.MILLIS.between()` was used to compute the
delay. This returns a value in milliseconds, but the result is then
passed to `Duration.ofNanos()`, treating it as nanoseconds.

This means the actual backoff would be ~1,000,000x shorter than the
server requested (e.g., a 5-second Retry-After would become 5
microseconds instead of 5 seconds).

The other two code paths (Retry-After-Ms and numeric Retry-After)
correctly convert their values to nanoseconds before reaching the
`Duration.ofNanos()` call. This fix changes `ChronoUnit.MILLIS` to
`ChronoUnit.NANOS` so all three paths produce consistent nanosecond
values.
@MaxwellCalkin MaxwellCalkin requested a review from a team as a code owner March 8, 2026 22:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant