Repository navigation
Major Changes
-
1a84633: SMOODEV-3375: Retries never duplicate a side effect: they now check the HTTP method, and timeouts cancel the attempt. This changes a default, so it is a major release.
What was wrong. Retries ignored the HTTP method. A
POSTthat timed out or got a 429/5xx was sent again, up to twice more. In TypeScript the per-attempt timeout (mollitia'sTimeoutmodule) did not stop the losing request, so the first attempt kept running on the server while the retry sent it again. Image generation takes 20-50s against the 10s default timeout, so it billed three images and returned nothing. Any non-idempotent call (CRM writes, message sends, payments) could run its side effect more than once.New default, in all five languages:
- Only idempotent methods are retried:
GET,HEAD,OPTIONS,TRACE,PUTandDELETE(RFC 9110 §9.2.2). POST,PATCHand any other method make exactly one attempt. You get that attempt's own error (HTTPResponseError/TimeoutErrorand the equivalents in other languages), not the retries-exhausted wrapper.- A 429 with
Retry-Afteron aPOSTis not retried either. onRejectionis never called for these requests, so it cannot turn their retries back on.
There are two ways to opt back in:
- Send a non-empty
Idempotency-Keyheader, in any casing. The server then deduplicates. - Set
retry: { allowNonIdempotent: true }. The name in each language:- TypeScript:
allowNonIdempotent - Python and Rust:
allow_non_idempotent - Go and .NET:
AllowNonIdempotent
- TypeScript:
Eligibility is checked after the pre-request hooks and the auth provider run, so a hook can add the key. The client-side rate limiter's retry loop is unchanged, because it rejects requests before anything is sent.
Timeouts cancel the attempt. TypeScript now aborts each attempt with an
AbortController, combined with the caller's ownsignal, so the connection is closed before any retry. Afetchthat ignoressignalstill times out on schedule. Rust, Go, Python and .NET already cancelled the attempt. A new test in every language proves the first attempt's connection closes. Go now also waits for the cancelled attempt to finish before it retries. Python now puts a hard deadline on each attempt withasyncio.timeout. Before, httpx only had per-phase timeouts, so a server that kept sending bytes slowly never timed out.Also fixed: in TypeScript, a request the caller aborted is no longer retried.
Other API changes:
- TypeScript:
- New exports:
isIdempotentMethod,isRetryEligible,IDEMPOTENCY_KEY_HEADERand theRetryOptionstype. options.retryandFetchBuilder.withRetrynow take a partial. It is merged over the defaults, soretry: { allowNonIdempotent: true }keeps every other default.- The
signalthatfetchreceives now combines the caller's signal with the timeout's. It is no longer the caller's own object.
- New exports:
- Rust:
RetryOptionsgainsallow_non_idempotent. This breaks existingRetryOptions { .. }struct literals.RetryOptionsnow implementsDefault, so literals can end with..Default::default()from now on.- New:
is_idempotent_method,is_retry_eligibleandIDEMPOTENCY_KEY_HEADER.
- Go:
- New:
RetryOptions.AllowNonIdempotent,IsIdempotentMethodandIdempotencyKeyHeader. - The module path moves to
/v4.
- New:
- Python:
- New:
RetryOptions.allow_non_idempotent. is_idempotent_methodandIDEMPOTENCY_KEY_HEADERare exported.
- New:
- .NET:
- New on
RetryPolicy:AllowNonIdempotent,IsIdempotentMethod,IsRetryEligibleandIdempotencyKeyHeader. Microsoft.SourceLink.GitHubgoes to 10.0.303. 8.0.0 pulls inMicrosoft.Build.Tasks.Git8.0.0, which has an advisory against it (GHSA-23fw-v26w-5fgq), and a clean restore now fails with NU1902.
- New on
Why a major version:
- A caller that relied on
POSTretries quietly loses them. - In Rust, adding a field to a struct that callers construct breaks their code. The 3.7.1 entry below explains why that must not ship as a minor.
The shared
spec/retry-idempotency-corpus.jsonpins the rule. Each language's tests run it against a real local server and count the requests the server received. - Only idempotent methods are retried: