Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 0 additions & 55 deletions .changeset/method-aware-retries.md

This file was deleted.

52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,57 @@
# @smooai/fetch

## 4.0.0

### 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 `POST` that timed out or got a 429/5xx was sent again, up to twice more. In TypeScript the per-attempt timeout (mollitia's `Timeout` module) 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`, `PUT` and `DELETE` (RFC 9110 §9.2.2).
- **`POST`, `PATCH` and any other method make exactly one attempt.** You get that attempt's own error (`HTTPResponseError` / `TimeoutError` and the equivalents in other languages), not the retries-exhausted wrapper.
- **A 429 with `Retry-After` on a `POST` is not retried either.**
- **`onRejection` is 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-Key` header, 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`

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 own `signal`, so the connection is closed before any retry. A `fetch` that ignores `signal` still 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 with `asyncio.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_HEADER` and the `RetryOptions` type.
- `options.retry` and `FetchBuilder.withRetry` now take a partial. It is merged over the defaults, so `retry: { allowNonIdempotent: true }` keeps every other default.
- The `signal` that `fetch` receives now combines the caller's signal with the timeout's. It is no longer the caller's own object.
- **Rust:**
- `RetryOptions` gains `allow_non_idempotent`. This breaks existing `RetryOptions { .. }` struct literals.
- `RetryOptions` now implements `Default`, so literals can end with `..Default::default()` from now on.
- New: `is_idempotent_method`, `is_retry_eligible` and `IDEMPOTENCY_KEY_HEADER`.
- **Go:**
- New: `RetryOptions.AllowNonIdempotent`, `IsIdempotentMethod` and `IdempotencyKeyHeader`.
- The module path moves to `/v4`.
- **Python:**
- New: `RetryOptions.allow_non_idempotent`.
- `is_idempotent_method` and `IDEMPOTENCY_KEY_HEADER` are exported.
- **.NET:**
- New on `RetryPolicy`: `AllowNonIdempotent`, `IsIdempotentMethod`, `IsRetryEligible` and `IdempotencyKeyHeader`.
- `Microsoft.SourceLink.GitHub` goes to 10.0.303. 8.0.0 pulls in `Microsoft.Build.Tasks.Git` 8.0.0, which has an advisory against it (GHSA-23fw-v26w-5fgq), and a clean restore now fails with NU1902.

**Why a major version:**
- A caller that relied on `POST` retries 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.json` pins the rule. Each language's tests run it against a real local server and count the requests the server received.

## 3.7.1

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion dotnet/SmooAI.Fetch/SmooAI.Fetch.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<NoWarn>$(NoWarn);CS1591</NoWarn>

<PackageId>SmooAI.Fetch</PackageId>
<Version>3.7.1</Version>
<Version>4.0.0</Version>
<Authors>SmooAI</Authors>
<Company>SmooAI</Company>
<Description>Resilient HTTP client for .NET with Polly-based retry, timeouts, typed JSON responses, and auth token injection. Port of @smooai/fetch.</Description>
Expand Down
2 changes: 1 addition & 1 deletion go/fetch/go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/SmooAI/fetch/go/fetch/v3
module github.com/SmooAI/fetch/go/fetch/v4

go 1.23.0

Expand Down
2 changes: 1 addition & 1 deletion go/fetch/version.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package fetch

// Version is the current version of the smooai-fetch Go package.
const Version = "3.7.1"
const Version = "4.0.0"
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@smooai/fetch",
"version": "3.7.1",
"version": "4.0.0",
"description": "A powerful fetch client library built on top of the native `fetch` API, designed for both Node.js and browser environments. Features built-in support for retries, timeouts, rate limiting, circuit breaking, and Standard Schema validation.",
"homepage": "https://github.com/SmooAI/fetch#readme",
"bugs": {
Expand Down
2 changes: 1 addition & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "smooai-fetch"
version = "3.7.1"
version = "4.0.0"
description = "A resilient HTTP fetch client with retries, timeouts, rate limiting, and circuit breaking."
# readme = "README.md"
authors = [{ name = "SmooAI", email = "brent@smooai.com" }]
Expand Down
2 changes: 1 addition & 1 deletion python/src/smooai_fetch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
and circuit breaking.
"""

__version__ = "3.7.1"
__version__ = "4.0.0"

# Core client
# Builder
Expand Down
2 changes: 1 addition & 1 deletion rust/fetch/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rust/fetch/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "smooai-fetch"
version = "3.7.1"
version = "4.0.0"
edition = "2021"
description = "A resilient HTTP fetch client with retries, timeouts, rate limiting, and circuit breaking."
license = "MIT"
Expand Down
Loading