Skip to content

Bug: onError retry path has no backoff delay #3895

@KyleAMathews

Description

@KyleAMathews

Problem

When onError returns {} to retry after a 4xx error, #start() recurses immediately with zero delay and fresh backoff state — creating a tight loop that can generate thousands of requests/second.

The inner createFetchWithBackoff only handles 5xx/network errors. When a 4xx triggers the onError callback and the user returns {} to retry, the code jumps straight to this.#started = false; await this.#start() with no delay whatsoever.

Expected behavior

Consecutive onError retries should apply exponential backoff (using the user's backoffOptions), with the first retry remaining immediate (since the user may have just refreshed an auth token in onError).

Proposed fix

  • Add a #consecutiveOnErrorRetries counter field on ShapeStream
  • Reset to 0 on successful response (in both #requestShapeLongPoll and #requestShapeSSE)
  • Before the retry in #start, if onErrorAttempt > 0: compute exponential backoff with full jitter using user's backoffOptions, honor retry-after header as floor, await the delay
  • Import parseRetryAfterHeader from ./fetch to read retry-after headers from FetchError

Behavioral summary

Scenario Before After
401 → onError refreshes token → retry Instant Instant (1st), then backoff
Persistent 4xx + onError: () => ({}) Tight loop, 1000s req/s Exponential backoff to 60s
Successful onError retry N/A Resets counter to 0

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