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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions