Skip to content

RetryWithExponentialBackoff appears to mishandle Retry-After header #112

@LocNguyenSGU

Description

@LocNguyenSGU

Summary

RetryWithExponentialBackoff appears to handle Retry-After incorrectly in a way that can cause retries to happen earlier than the server requested.

There seem to be two independent issues in the current implementation:

  1. It reads headers['Retry-After'] instead of the normalized lowercase header key.
  2. It compares elapsed time in milliseconds against a parsed Retry-After value that is treated like seconds.

Current Behavior

In api/apis.ts, the retry logic currently does this:

const responseHeaders = headers || {}
lastRequestRetryAfter = responseHeaders['Retry-After']
if (lastRequestRetryAfter) {
    lastRequestRetryAfter = parseInt(lastRequestRetryAfter, 10)
}

and later:

const retryAfterValueLapsed = (!lastRequestRetryAfter ||
    currentTime - lastRequestTimestamp > lastRequestRetryAfter)

This creates two problems:

1) Header lookup likely misses the value

In JavaScript object access, headers['Retry-After'] and headers['retry-after'] are different keys.

For example:

const headers = { 'retry-after': '10' }
headers['Retry-After'] // undefined
headers['retry-after'] // '10'

So the code can ignore the server-provided Retry-After value entirely.

2) Time units are mismatched

Date.now() values are in milliseconds, but a numeric Retry-After header is parsed as seconds.

So if the server returns:

Retry-After: 10

then the code effectively compares:

elapsedMilliseconds > 10

instead of waiting roughly 10 seconds.

That means retries can happen after ~10ms rather than ~10s.

Expected Behavior

If a retryable response includes Retry-After, the client should:

  1. read the header value reliably
  2. interpret numeric values using the correct units
  3. avoid retrying until the server-requested delay has elapsed

Evidence

Relevant code in api/apis.ts:

const responseHeaders = headers || {}
lastRequestRetryAfter = responseHeaders['Retry-After']
if (lastRequestRetryAfter) {
    lastRequestRetryAfter = parseInt(lastRequestRetryAfter, 10)
}
lastRequestTimestamp = Date.now()

and:

const retryAfterValueLapsed = (!lastRequestRetryAfter ||
    currentTime - lastRequestTimestamp > lastRequestRetryAfter)

Reproduction

  1. Use RetryWithExponentialBackoff with a request that receives a retryable response such as 429.
  2. Return a Retry-After header with a numeric value, e.g. 10.
  3. Observe that:
    • the value may be ignored if the header is exposed as lowercase
    • even if used, the elapsed-time comparison is done in milliseconds vs seconds
  4. The next retry can happen much earlier than intended.

Duplicate Check

  • Open issues checked: no direct match found
  • Closed issues checked: found #7, but that older issue was closed as fixed in 2.0.0 and does not appear to describe this current logic problem directly
  • Recent PRs checked: no direct match found

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions