Files
2026-05-27 23:47:53 +01:00

3.8 KiB
Raw Permalink Blame History

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 any 5xx.
  • parseRetryAfter(value, now) — delta-seconds and HTTP-date forms; capped at RETRY_AFTER_MAX_DELAY_MS (60s).
  • retryWithBackoff(fn, options?) — runs fn up to maxAttempts, sleeps between transient outcomes, supports cancellation.
  • Injectable TimersLike so 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 to globalThis.setTimeout/clearTimeout.
  • options.signal? — cancels the wait between attempts; pre-aborted signal short-circuits with STORAGE_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.
  • sleep listens 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 a Response) 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 > 0 at the boundary so the path is defensive.
  • Mid-wait abort throws UPSTREAM_ERROR (with cause = signal.reason) rather than a separate ABORTED code because the Rackspace error registry deliberately keeps abort under the upstream umbrella — operators monitoring "request canceled" already filter on cause-shape, not a separate code.