denoland / denoland/std

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

Open
#7,296 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
3.6k
Forks
681
PR merge metrics
No merged PRs in 30d

Description

**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:

- #7298 adds `isRetriableFetchError`
- #7299 adds `parseRetryAfter`

**1. A predicate for retry's `isRetriable` hook**

```ts
// 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:

```ts
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**

```ts
// 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` (#7132) is still unstable. Direct integration can follow once it stabilizes. The structural check covers errors with a direct `status` property today.

Contributor guide

Open the contributing guide

Research direction

Begin with http/unstable_is_retriable_fetch_error.ts and http/unstable_retry_after.ts, and compare the retry isRetriable example in async/retry.ts. Done means both unstable APIs cover the listed fetch/error and Retry-After cases without changing stable retry behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
deno, typescript
Domain
api, networking
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.