Files
skills/docs/persistence/storage/storage-cdn-bunnycdn/src/retry-helper/retry-helper.md
2026-05-29 08:20:56 +01:00

3.5 KiB
Raw Blame History

retry-helper

(no submodules)

Purpose

Coordinates retry logic for HTTP requests against BunnyCDN Storage Zones and the Bunny.net management API. Distinguishes transient outcomes (408/425/429/5xx) from fatal ones, honours Retry-After when the server provides it, falls back to exponential backoff with a configurable cap, and surfaces exhausted retries as STORAGE_CDN_BUNNYCDN.UPSTREAM_ERROR with zone/region context.

Structurally similar to the sibling storage-cdn-rackspace retry helper but lives in this package so error codes carry BunnyCDN-specific diagnostics.

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 under a FakeClock.
  • Exhausted retries surface as STORAGE_CDN_BUNNYCDN.UPSTREAM_ERROR.

Usage: Basic

import { retryWithBackoff } from "@suaveplan/storage-cdn-bunnycdn";

const response = await retryWithBackoff(() => fetch("https://storage.bunnycdn.com/myz/k"));

Usage: Advanced

import { retryWithBackoff } from "@suaveplan/storage-cdn-bunnycdn";

const response = await retryWithBackoff(
    () => fetch(url, { signal, headers: { AccessKey: process.env["BUNNY_KEY"]! } }),
    {
        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_BUNNYCDN.UPSTREAM_ERROR.

Returns the last response after exhaustion or throws on synthesised exhaustion / 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 an 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.
  • 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 BunnyCDN error registry deliberately keeps abort under the upstream umbrella — operators monitoring "request canceled" already filter on cause-shape, not a separate code.