Skip to content

Reject a Retry-After that overflows the Duration conversion - #297

Open
dngr2 wants to merge 1 commit into
hashicorp:mainfrom
dngr2:fix/retry-after-seconds-overflow
Open

Reject a Retry-After that overflows the Duration conversion#297
dngr2 wants to merge 1 commit into
hashicorp:mainfrom
dngr2:fix/retry-after-seconds-overflow

Conversation

@dngr2

@dngr2 dngr2 commented Aug 16, 2026

Copy link
Copy Markdown

Description

parseRetryAfterHeader converts a seconds value with time.Second * time.Duration(sleep). A Duration counts nanoseconds, so any value above math.MaxInt64/time.Second (9223372036, about 292 years) wraps and comes back negative:

Retry-After: 120                    -> 2m0s                       ok=true
Retry-After: 9223372036             -> 2562047h47m16s             ok=true
Retry-After: 9223372037             -> -2562047h47m16s            ok=true   <-- negative
Retry-After: 9223372036854775807    -> -1s                        ok=true   <-- negative

The header is reported as successfully parsed, so DefaultBackoff and RateLimitLinearJitterBackoff hand that negative duration back as the wait. client.go:784 does:

timer := time.NewTimer(wait)

A negative duration makes the timer fire immediately. The client retries with no delay and keeps doing so until RetryMax is spent — so a server asking to be left alone for a long time is answered with a burst of requests instead. That is the opposite of what the header, and this library, are for.

The existing guard covers a negative value in the header:

if sleep < 0 { // a negative sleep doesn't make sense
    return 0, false
}

but not a positive value that becomes negative in the conversion.

Related Issue

None open that I could find; noticed while reading the backoff paths.

How Has This Been Tested?

Three cases added to the existing TestParseRetryAfterHeader table:

Case Header Expected
seconds-overflow 9223372037 not parseable
seconds-overflow-max-int64 9223372036854775807 not parseable
seconds-largest-representable 9223372036 9223372036s, parseable

The third pins the boundary so the guard cannot be tightened by accident and start rejecting valid values.

Values past the representable range are treated as unparseable, so the caller falls back to its usual exponential backoff — the same thing that already happens for an empty, malformed or date-in-the-past header. Clamping to MaxInt64 instead would mean a ~292 year wait, which seemed worse than ignoring an absurd header.

go test ./... passes (22.8s). Removing the guard fails the two overflow cases and leaves the boundary case passing.

Go 1.23.4, Linux.

parseRetryAfterHeader converts a seconds value with
`time.Second * time.Duration(sleep)`. A Duration counts nanoseconds, so
any value above math.MaxInt64/time.Second (9223372036, about 292 years)
wraps and comes back negative:

    Retry-After: 9223372037           -> -2562047h47m16s
    Retry-After: 9223372036854775807  -> -1s

The header is reported as parsed, so DefaultBackoff and
RateLimitLinearJitterBackoff return that negative duration as the wait.
time.NewTimer fires immediately on a negative duration, so the client
retries with no delay at all and keeps doing so until RetryMax is spent.
A server asking to be left alone for a long time is answered with a burst
of requests instead, which is the opposite of backing off.

The existing `sleep < 0` guard covers a negative value in the header but
not a positive one that becomes negative in the conversion. Values past
the representable range are now treated as unparseable, so the caller
falls back to its usual exponential backoff, which is what happens for
any other unusable header.

Adds table cases for the first overflowing value, MaxInt64, and the
largest value that still converts cleanly. The first two fail without
the guard.
@dngr2
dngr2 requested a review from a team as a code owner August 16, 2026 03:26
@hashicorp-cla-app

hashicorp-cla-app Bot commented Aug 16, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@hashicorp-cla-app

Copy link
Copy Markdown

CLA assistant check

Thank you for your submission! We require that all contributors sign our Contributor License Agreement ("CLA") before we can accept the contribution. Read and sign the agreement

Learn more about why HashiCorp requires a CLA and what the CLA includes

Have you signed the CLA already but the status is still pending? Recheck it.

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