Skip to content

AbortSignal does not cancel rate-limit retry backoff #2003

Description

@Osraka

Describe the bug

RequestOptions.signal cancels an active fetch, but it does not cancel the rate-limit backoff between attempts.

Both OpenSeaAPI.get and OpenSeaAPI.request pass the signal to _fetch, then call executeWithRateLimit with only the logger. After a 429/599 response, executeWithRateLimit waits in a non-abortable sleep(delayMs). If the caller aborts during that wait, the returned promise remains pending until the full Retry-After delay expires (currently capped at five minutes). Only the next _fetch attempt observes the already-aborted signal.

Observed states on v12.0.1:

Abort timing Current result
Before the first request Rejects immediately with Request aborted
During an active fetch The fetch signal is aborted
During 429/599 retry backoff Remains pending until the delay expires

Steps to reproduce

  1. Mock fetch to return HTTP 429 with Retry-After: 30.
  2. Call api.get(path, {}, { signal: controller.signal }) (the same behavior occurs through request/post).
  3. After the first attempt enters the retry delay, call controller.abort().
  4. Observe that the API promise is still pending and no second fetch has run.
  5. Advance the retry timer by 30 seconds; only then does the next attempt observe the aborted signal and reject.

This is deterministic with Vitest fake timers: immediately after abort, the outcome remains pending; after advancing the backoff timer, it becomes rejected.

Expected behavior

Aborting the supplied signal while a request is waiting to retry should reject promptly and should not start another fetch attempt.

Environment

  • Package version: 12.0.1
  • Node.js version: 22.x
  • Operating system: macOS
  • Chain / network: not chain-specific

Additional context

A narrow, backward-compatible fix appears possible: allow RateLimitOptions to receive an optional AbortSignal, make only the retry delay abortable, and pass the existing request signal from get and request. This would preserve retry counts, Retry-After parsing, timeout behavior, and the existing Request aborted error shape.

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