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 |
Problem
When
onErrorreturns{}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
createFetchWithBackoffonly handles 5xx/network errors. When a 4xx triggers theonErrorcallback and the user returns{}to retry, the code jumps straight tothis.#started = false; await this.#start()with no delay whatsoever.Expected behavior
Consecutive
onErrorretries should apply exponential backoff (using the user'sbackoffOptions), with the first retry remaining immediate (since the user may have just refreshed an auth token inonError).Proposed fix
#consecutiveOnErrorRetriescounter field onShapeStream#requestShapeLongPolland#requestShapeSSE)#start, ifonErrorAttempt > 0: compute exponential backoff with full jitter using user'sbackoffOptions, honorretry-afterheader as floor, await the delayparseRetryAfterHeaderfrom./fetchto read retry-after headers fromFetchErrorBehavioral summary
onError: () => ({})