3.8 KiB
retry-helper
(no submodules)
Purpose
Coordinates retry logic for HTTP requests against Rackspace Cloud Files and the Rackspace CDN management API. Distinguishes transient outcomes (408/425/429/5xx) from fatal ones, honours Retry-After when the server provides it, and falls back to exponential backoff with a configurable cap. AbortSignal is plumbed through every wait so cancellation lands within milliseconds.
This is structurally similar to @suaveplan/storage-http's retry helper but lives in this package so the adapter can use Rackspace-specific error codes for exhausted retries — a STORAGE_CDN_RACKSPACE.UPSTREAM_ERROR surfaces with container and region context that the generic helper cannot carry.
Features
isTransientStatus(status)—408,425,429, or any5xx.parseRetryAfter(value, now)— delta-seconds and HTTP-date forms; capped atRETRY_AFTER_MAX_DELAY_MS(60s).retryWithBackoff(fn, options?)— runsfnup tomaxAttempts, sleeps between transient outcomes, supports cancellation.- Injectable
TimersLikeso tests pin behaviour without real timers. - Exhausted retries surface as
STORAGE_CDN_RACKSPACE.UPSTREAM_ERROR.
Usage: Basic
import { retryWithBackoff } from "@suaveplan/storage-cdn-rackspace";
const response = await retryWithBackoff(() => fetch("https://cdn.example.com/file.bin"));
Usage: Advanced
import { retryWithBackoff } from "@suaveplan/storage-cdn-rackspace";
const response = await retryWithBackoff(
() => fetch(url, { signal, headers: { "x-auth-token": token } }),
{
retry: { maxAttempts: 5, baseDelayMs: 200, maxDelayMs: 10_000, honourRetryAfter: true },
timers: fakeTimers,
signal,
},
);
API Reference
retryWithBackoff(fn, options?): Promise<Response>
fn: () => Promise<Response>— attempts the request.options.retry?—maxAttempts,baseDelayMs,maxDelayMs,honourRetryAfter.options.timers?— defaults toglobalThis.setTimeout/clearTimeout.options.signal?— cancels the wait between attempts; pre-aborted signal short-circuits withSTORAGE_CDN_RACKSPACE.UPSTREAM_ERROR.
Returns the last response after exhaustion (caller decides how to surface a final transient status) or throws on synthesised exhaustion (no response, no error captured) / abort.
isTransientStatus(status: number): boolean
True for 408, 425, 429, or any value in [500, 599].
parseRetryAfter(value: string | null, now: number): number | undefined
- Numeric value → delta-seconds × 1000 (capped at
RETRY_AFTER_MAX_DELAY_MS). - HTTP-date →
parseDate − now(clamped to 0 if in the past, capped on the high side). - Missing / empty / unparseable →
undefined.
Implementation Notes
- The Retry-After ceiling is distinct from
maxDelayMs; the latter governs exponential backoff, the former specifically defends against the "stall 1 hour" abuse pattern. sleeplistens to the abort event with{ once: true }and tears down both the abort listener and the underlying timer on either resolution path — no listener leaks.- The helper is
fetch-shaped (returns aResponse) rather than generic; HTTP semantics drive every retry decision, and a generic helper would duplicate the classifier. - Synthesised exhaustion (no response, no error) is only reachable when
maxAttempts === 0; the schema validates> 0at the boundary so the path is defensive. - Mid-wait abort throws
UPSTREAM_ERROR(withcause = signal.reason) rather than a separateABORTEDcode because the Rackspace error registry deliberately keeps abort under the upstream umbrella — operators monitoring "request canceled" already filter oncause-shape, not a separate code.