Skip to content

proposal(http): add isRetriableFetchError and a Retry-After parser #7296

Description

@tomas-zijdemans

Is your feature request related to a problem? Please describe.

retry() grew an isRetriable hook for exactly this job (#6196, stabilized in #6944), but std ships nothing to put in it. The retry JSDoc even shows users how to hand-roll the predicate: define your own HttpError, check status === 429 || status >= 500 (the "Only retry on specific error types" example in async/retry.ts).

In the wild that copy-paste goes worse. A work codebase I maintain rolls this predicate in three places, each one matching substrings in error.message ("429", "throttl", "timeout"). That false-positives the moment a payload containing "429" ends up in an error message. Everyone writing a fetch retry loop rebuilds this, and most versions are wrong in a different way.

Describe the solution you'd like

Two small unstable APIs in @std/http.

Implementation:

1. A predicate for retry's isRetriable hook

// http/unstable_is_retriable_fetch_error.ts
export function isRetriableFetchError(error: unknown): boolean;

Walks the value and its cause chain (depth-bounded, cycle-safe) and returns true for:

  • a thrown Response with status 408, 429, or any 5xx, including nonstandard codes such as Cloudflare's 522
  • an Error with an integer status property in 100-599, using the same status test. This covers the class in retry's own docs and other errors with a direct status property
  • a DOMException named TimeoutError, i.e. what AbortSignal.timeout() throws
  • a TypeError matching a known failed-fetch message (fetch failed on modern Deno, Node.js, and Bun, plus browser messages), or the error sending request prefix from older Deno releases. Never arbitrary substring search, for the reason above

Everything else is false, including AbortError: abort is caller intent, and retrying a cancelled request is a bug. False negatives are the safe direction, since the caller sees the original error immediately.

Usage:

import { retry } from "@std/async/retry";
import { isRetriableFetchError } from "@std/http/unstable-is-retriable-fetch-error";

const data = await retry(async () => {
  const res = await fetch(url);
  if (!res.ok) throw res;
  return await res.json();
}, { isRetriable: isRetriableFetchError });

2. A Retry-After parser

// http/unstable_retry_after.ts
export interface ParseRetryAfterOptions {
  now?: Date;
}

export function parseRetryAfter(
  value: string | null,
  options?: ParseRetryAfterOptions,
): number | null;

Handles both RFC 9110 §10.2.3 forms (delta-seconds and HTTP-date), returns milliseconds to wait, null when missing or unparseable. It takes the raw value from headers.get("retry-after"), with no dependency on the HEADER catalog. #7239 removes that catalog. Useful on its own today, and a prerequisite for retry() ever honoring the header. Honoring it needs a per-attempt delay hook on retry, which would be a separate proposal(async) if this lands. Nothing here touches stable retry behavior.

Why @std/http and not @std/async

The substance is all HTTP: status classification, header parsing, fetch error shapes. Since isRetriable is structurally (err: unknown) => boolean, nothing here imports @std/async. No circular dependency, and @std/async stays free of domain knowledge.

Describe alternatives you've considered

  • Status quo: keep pointing users at the docs example. That example is how the substring heuristics happen.
  • instanceof HttpError for the status check. HttpError (feat(http/unstable): HttpError #7132) is still unstable. Direct integration can follow once it stabilizes. The structural check covers errors with a direct status property today.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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