You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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.
Is your feature request related to a problem? Please describe.
retry()grew anisRetriablehook for exactly this job (#6196, stabilized in #6944), but std ships nothing to put in it. TheretryJSDoc even shows users how to hand-roll the predicate: define your ownHttpError, checkstatus === 429 || status >= 500(the "Only retry on specific error types" example inasync/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:
isRetriableFetchError#7298 addsisRetriableFetchErrorparseRetryAfter#7299 addsparseRetryAfter1. A predicate for retry's
isRetriablehookWalks the value and its
causechain (depth-bounded, cycle-safe) and returnstruefor:Responsewith status 408, 429, or any 5xx, including nonstandard codes such as Cloudflare's 522Errorwith an integerstatusproperty in 100-599, using the same status test. This covers the class inretry's own docs and other errors with a directstatuspropertyDOMExceptionnamedTimeoutError, i.e. whatAbortSignal.timeout()throwsTypeErrormatching a known failed-fetch message (fetch failedon modern Deno, Node.js, and Bun, plus browser messages), or theerror sending requestprefix from older Deno releases. Never arbitrary substring search, for the reason aboveEverything else is
false, includingAbortError: 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:
2. A
Retry-AfterparserHandles both RFC 9110 §10.2.3 forms (delta-seconds and HTTP-date), returns milliseconds to wait,
nullwhen missing or unparseable. It takes the raw value fromheaders.get("retry-after"), with no dependency on theHEADERcatalog. #7239 removes that catalog. Useful on its own today, and a prerequisite forretry()ever honoring the header. Honoring it needs a per-attempt delay hook onretry, which would be a separateproposal(async)if this lands. Nothing here touches stableretrybehavior.Why
@std/httpand not@std/asyncThe substance is all HTTP: status classification, header parsing, fetch error shapes. Since
isRetriableis structurally(err: unknown) => boolean, nothing here imports@std/async. No circular dependency, and@std/asyncstays free of domain knowledge.Describe alternatives you've considered
instanceof HttpErrorfor 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 directstatusproperty today.