proposal(http): add `isRetriableFetchError` and a `Retry-After` parser
- 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
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