@fetchkit/ffetch 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,230 +1,374 @@
1
- ![npm](https://img.shields.io/npm/v/@fetchkit/ffetch)
2
- ![Downloads](https://img.shields.io/npm/dm/@fetchkit/ffetch)
3
- ![GitHub stars](https://img.shields.io/github/stars/fetch-kit/ffetch?style=social)
4
-
5
- ![Build](https://github.com/fetch-kit/ffetch/actions/workflows/ci.yml/badge.svg)
6
- ![codecov](https://codecov.io/gh/fetch-kit/ffetch/branch/main/graph/badge.svg)
7
-
8
- ![MIT](https://img.shields.io/npm/l/@fetchkit/ffetch)
9
- ![bundlephobia](https://badgen.net/bundlephobia/minzip/@fetchkit/ffetch)
10
- ![Types](https://img.shields.io/npm/types/@fetchkit/ffetch)
11
-
12
- # @fetchkit/ffetch
13
-
14
- **A production-ready TypeScript-first drop-in replacement for native fetch, or any fetch-compatible implementation.**
15
-
16
- ffetch can wrap any fetch-compatible implementation (native fetch, node-fetch, undici, or framework-provided fetch), making it flexible for SSR, edge, and custom environments.
17
-
18
- ffetch uses a plugin architecture for optional features, so you only include what you need.
19
-
20
- **Key Features:**
21
-
22
- - **Timeouts** per-request or global
23
- - **Retries** exponential backoff + jitter
24
- - **Plugin architecture**extensible lifecycle-based plugins for optional behavior
25
- - **Hooks** – logging, auth, metrics, request/response transformation
26
- - **Pending requests** – real-time monitoring of active requests
27
- - **Per-request overrides** – customize behavior on a per-request basis
28
- - **Universal** – Node.js, Browser, Cloudflare Workers, React Native
29
- - **Zero runtime deps** – ships as dual ESM/CJS
30
- - **Configurable error handling** – custom error types and `throwOnHttpError` flag to throw on HTTP errors
31
- - **Circuit breaker plugin (optional, prebuilt)** – automatic failure protection
32
- - **Deduplication plugin (optional, prebuilt)** – automatic deduping of in-flight identical requests
33
-
34
- ## Quick Start
35
-
36
- ### Install
37
-
38
- ```bash
39
- npm install @fetchkit/ffetch
40
- ```
41
-
42
- ### Basic Usage
43
-
44
- ```typescript
45
- import { createClient } from '@fetchkit/ffetch'
46
- import { dedupePlugin } from '@fetchkit/ffetch/plugins/dedupe'
47
-
48
- // Create a client with timeout, retries, and deduplication plugin
49
- const api = createClient({
50
- timeout: 5000,
51
- retries: 3,
52
- plugins: [dedupePlugin()],
53
- retryDelay: ({ attempt }) => 2 ** attempt * 100 + Math.random() * 100,
54
- })
55
-
56
- // Make requests
57
- const response = await api('https://api.example.com/users')
58
- const data = await response.json()
59
-
60
- // Deduplication example: these two requests will be deduped
61
- const p1 = api('https://api.example.com/data')
62
- const p2 = api('https://api.example.com/data')
63
- const [r1, r2] = await Promise.all([p1, p2])
64
- // Only one fetch will occur; both promises resolve to the same response
65
- ```
66
-
67
- ### Using a Custom fetchHandler (SSR, metaframeworks, or polyfills)
68
-
69
- ```typescript
70
- // Example: SvelteKit, Next.js, Nuxt, or node-fetch
71
- import { createClient } from '@fetchkit/ffetch'
72
-
73
- // Pass your framework's fetch implementation
74
- const api = createClient({
75
- fetchHandler: fetch, // SvelteKit/Next.js/Nuxt provide their own fetch
76
- timeout: 5000,
77
- })
78
-
79
- // Or use node-fetch/undici in Node.js
80
- import nodeFetch from 'node-fetch'
81
- const apiNode = createClient({ fetchHandler: nodeFetch })
82
-
83
- // All ffetch features work identically
84
- const response = await api('/api/data')
85
- ```
86
-
87
- ### Advanced Example
88
-
89
- ```typescript
90
- // Production-ready client with error handling and monitoring
91
- import { createClient } from '@fetchkit/ffetch'
92
- import { dedupePlugin } from '@fetchkit/ffetch/plugins/dedupe'
93
- import { circuitPlugin } from '@fetchkit/ffetch/plugins/circuit'
94
-
95
- const client = createClient({
96
- timeout: 10000,
97
- retries: 2,
98
- fetchHandler: fetch, // Use custom fetch if needed
99
- plugins: [
100
- dedupePlugin({
101
- hashFn: (params) => `${params.method}|${params.url}|${params.body}`,
102
- ttl: 30_000,
103
- sweepInterval: 5_000,
104
- }),
105
- circuitPlugin({
106
- threshold: 5,
107
- reset: 30_000,
108
- onCircuitOpen: (req) => console.warn('Circuit opened due to:', req.url),
109
- onCircuitClose: (req) => console.info('Circuit closed after:', req.url),
110
- }),
111
- ],
112
- hooks: {
113
- before: async (req) => console.log('→', req.url),
114
- after: async (req, res) => console.log('←', res.status),
115
- onError: async (req, err) => console.error('Error:', err.message),
116
- },
117
- })
118
-
119
- try {
120
- const response = await client('/api/data')
121
-
122
- // Check HTTP status manually (like native fetch)
123
- if (!response.ok) {
124
- console.log('HTTP error:', response.status)
125
- return
126
- }
127
-
128
- const data = await response.json()
129
- console.log('Active requests:', client.pendingRequests.length)
130
- } catch (err) {
131
- if (err instanceof TimeoutError) {
132
- console.log('Request timed out')
133
- } else if (err instanceof RetryLimitError) {
134
- console.log('Request failed after retries')
135
- }
136
- }
137
- ```
138
-
139
- ### Custom Error Handling with `throwOnHttpError`
140
-
141
- Native `fetch`'s controversial behavior of not throwing errors for HTTP error status codes (4xx, 5xx) can lead to overlooked errors in applications. By default, `ffetch` follows this same pattern, returning a `Response` object regardless of the HTTP status code. However, with the `throwOnHttpError` flag, developers can configure `ffetch` to throw an `HttpError` for HTTP error responses, making error handling more explicit and robust. Note that this behavior is affected by retries and the circuit breaker - full details are explained in the [Error Handling documentation](./docs/errorhandling.md).
142
-
143
- ## Documentation
144
-
145
- | Topic | Description |
146
- | --------------------------------------------- | ------------------------------------------------------------------------- |
147
- | **[Complete Documentation](./docs/index.md)** | **Start here** - Documentation index and overview |
148
- | **[API Reference](./docs/api.md)** | Complete API documentation and configuration options |
149
- | **[Plugin Architecture](./docs/plugins.md)** | Plugin lifecycle, custom plugin authoring, and integration patterns |
150
- | **[Deduplication](./docs/deduplication.md)** | How deduplication works, hash config, optional TTL cleanup, limitations |
151
- | **[Error Handling](./docs/errorhandling.md)** | Strategies for managing errors, including `throwOnHttpError` |
152
- | **[Advanced Features](./docs/advanced.md)** | Per-request overrides, pending requests, circuit breakers, custom errors |
153
- | **[Hooks & Transformation](./docs/hooks.md)** | Lifecycle hooks, authentication, logging, request/response transformation |
154
- | **[Usage Examples](./docs/examples.md)** | Real-world patterns: REST clients, GraphQL, file uploads, microservices |
155
- | **[Compatibility](./docs/compatibility.md)** | Browser/Node.js support, polyfills, framework integration |
156
-
157
- ## Environment Requirements
158
-
159
- `ffetch` works best with native `AbortSignal.any` support:
160
-
161
- - **Node.js 20.6+** (native `AbortSignal.any`)
162
- - **Modern browsers with `AbortSignal.any`** (for example: Chrome 117+, Firefox 117+, Safari 17+, Edge 117+)
163
-
164
- If your environment does not support `AbortSignal.any` (Node.js < 20.6, older browsers), you can still use ffetch by installing an `AbortSignal.any` polyfill. `AbortSignal.timeout` is optional because ffetch includes an internal timeout fallback. See the [compatibility guide](./docs/compatibility.md) for instructions.
165
-
166
- **Custom fetch support:**
167
- You can pass any fetch-compatible implementation (native fetch, node-fetch, undici, SvelteKit, Next.js, Nuxt, or a polyfill) via the `fetchHandler` option. This makes ffetch fully compatible with SSR, edge, metaframework environments, custom backends, and test runners.
168
-
169
- #### "AbortSignal.any is not a function"
170
-
171
- Solution: Install a polyfill for `AbortSignal.any`
172
-
173
- ```bash
174
- npm install abort-controller-x
175
- ```
176
-
177
- ## CDN Usage
178
-
179
- ```html
180
- <script type="module">
181
- import { createClient } from 'https://unpkg.com/@fetchkit/ffetch/dist/index.min.js'
182
-
183
- const api = createClient({ timeout: 5000 })
184
- const data = await api('/api/data').then((r) => r.json())
185
- </script>
186
- ```
187
-
188
- ## Deduplication Limitations
189
-
190
- - Deduplication is **off** by default. Enable it via `plugins: [dedupePlugin()]`.
191
- - The default hash function is `dedupeRequestHash`, which handles common body types and skips deduplication for streams and FormData.
192
- - Optional stale-entry cleanup: `dedupePlugin({ ttl, sweepInterval })` enables map-entry eviction. TTL eviction only removes dedupe keys; it does not reject already in-flight promises.
193
- - **Stream bodies** (`ReadableStream`, `FormData`): Deduplication is skipped for requests with these body types, as they cannot be reliably hashed or replayed.
194
- - **Non-idempotent requests**: Use deduplication with caution for non-idempotent methods (e.g., POST), as it may suppress multiple intended requests.
195
- - **Custom hash function**: Ensure your hash function uniquely identifies requests to avoid accidental deduplication.
196
-
197
- See [deduplication.md](./docs/deduplication.md) for full details.
198
-
199
- ## Fetch vs. Axios vs. `ffetch`
200
-
201
- | Feature | Native Fetch | Axios | ffetch |
202
- | -------------------- | ------------------------- | -------------------- | -------------------------------------------------------------------------------------- |
203
- | Timeouts | ❌ Manual AbortController | ✅ Built-in | ✅ Built-in with fallbacks |
204
- | Retries | ❌ Manual implementation | ❌ Manual or plugins | ✅ Smart exponential backoff |
205
- | Plugin Architecture | Not available | ⚠️ Interceptors only | ✅ First-class plugin pipeline (optional built-in + custom plugins) |
206
- | Circuit Breaker | ❌ Not available | ❌ Manual or plugins | ✅ Automatic failure protection |
207
- | Deduplication | Not available | ❌ Not available | ✅ Automatic deduplication of in-flight identical requests |
208
- | Request Monitoring | Manual tracking | Manual tracking | ✅ Built-in pending requests |
209
- | Error Types | Generic errors | ⚠️ HTTP errors only | ✅ Specific error classes |
210
- | TypeScript | ⚠️ Basic types | ⚠️ Basic types | ✅ Full type safety |
211
- | Hooks/Middleware | ❌ Not available | ✅ Interceptors | ✅ Comprehensive lifecycle hooks |
212
- | Bundle Size | ✅ Native (0kb) | ❌ ~13kb minified | ✅ ~3kb minified |
213
- | Modern APIs | ✅ Web standards | ❌ XMLHttpRequest | ✅ Fetch + modern features |
214
- | Custom Fetch Support | ❌ No (global only) | ❌ No | ✅ Yes (wrap any fetch-compatible implementation, including framework or custom fetch) |
215
-
216
- ## Join the Community
217
-
218
- Got questions, want to discuss features, or share examples? Join the **Fetch-Kit Discord server**:
219
-
220
- [![Discord](https://img.shields.io/badge/Discord-Join_Fetch--Kit-7289DA?logo=discord&logoColor=white)](https://discord.gg/sdyPBPCDUg)
221
-
222
- ## Contributing
223
-
224
- - **Issues**: [GitHub Issues](https://github.com/fetch-kit/ffetch/issues)
225
- - **Pull Requests**: [GitHub PRs](https://github.com/fetch-kit/ffetch/pulls)
226
- - **Documentation**: Found in `./docs/` - PRs welcome!
227
-
228
- ## License
229
-
230
- MIT © 2025 gkoos
1
+ ![npm](https://img.shields.io/npm/v/@fetchkit/ffetch)
2
+ ![Downloads](https://img.shields.io/npm/dm/@fetchkit/ffetch)
3
+ ![GitHub stars](https://img.shields.io/github/stars/fetch-kit/ffetch?style=social)
4
+
5
+ ![Build](https://github.com/fetch-kit/ffetch/actions/workflows/ci.yml/badge.svg)
6
+ ![codecov](https://codecov.io/gh/fetch-kit/ffetch/branch/main/graph/badge.svg)
7
+
8
+ ![MIT](https://img.shields.io/npm/l/@fetchkit/ffetch)
9
+ ![bundlephobia](https://badgen.net/bundlephobia/minzip/@fetchkit/ffetch)
10
+ ![Types](https://img.shields.io/npm/types/@fetchkit/ffetch)
11
+
12
+ # @fetchkit/ffetch
13
+
14
+ **A production-ready TypeScript-first drop-in replacement for native fetch, or any fetch-compatible implementation.**
15
+
16
+ ffetch can wrap any fetch-compatible implementation (native fetch, node-fetch, undici, or framework-provided fetch), making it flexible for SSR, edge, and custom environments.
17
+
18
+ ffetch uses a plugin architecture for optional features, so you only include what you need.
19
+
20
+ ## Why ffetch
21
+
22
+ - Keep native fetch ergonomics, add production safety (timeouts, retries, error strategy).
23
+ - Keep your runtime flexibility (use global fetch or any fetch-compatible handler).
24
+ - Keep your bundle lean **~3kb minified** (optional plugins, zero runtime dependencies).
25
+
26
+ ## Table of Contents
27
+
28
+ - [@fetchkit/ffetch](#fetchkitffetch)
29
+ - [Why ffetch](#why-ffetch)
30
+ - [Table of Contents](#table-of-contents)
31
+ - [Key Features](#key-features)
32
+ - [Built-in Plugins at a Glance](#built-in-plugins-at-a-glance)
33
+ - [What Problems Does ffetch Solve?](#what-problems-does-ffetch-solve)
34
+ - [Quick Start](#quick-start)
35
+ - [Install](#install)
36
+ - [Basic Setup](#basic-setup)
37
+ - [Production Setup with Plugins](#production-setup-with-plugins)
38
+ - [Why not only native fetch?](#why-not-only-native-fetch)
39
+ - [Common Recipes](#common-recipes)
40
+ - [Using a Custom fetchHandler (SSR, metaframeworks, or polyfills)](#using-a-custom-fetchhandler-ssr-metaframeworks-or-polyfills)
41
+ - [Advanced Example](#advanced-example)
42
+ - [Custom Error Handling with `throwOnHttpError`](#custom-error-handling-with-throwonhttperror)
43
+ - [Documentation](#documentation)
44
+ - [Environment Requirements](#environment-requirements)
45
+ - ["AbortSignal.any is not a function"](#abortsignalany-is-not-a-function)
46
+ - [CDN Usage](#cdn-usage)
47
+ - [Deduplication Limitations](#deduplication-limitations)
48
+ - [Fetch vs. Axios vs. ky vs. `ffetch`](#fetch-vs-axios-vs-ky-vs-ffetch)
49
+ - [Try ffetch in Action](#try-ffetch-in-action)
50
+ - [Join the Community](#join-the-community)
51
+ - [Contributing](#contributing)
52
+ - [License](#license)
53
+
54
+ ## Key Features
55
+
56
+ - **Timeouts** – per-request or global
57
+ - **Retries** exponential backoff + jitter
58
+ - **Abort-aware retries** aborting during backoff cancels immediately
59
+ - **Plugin architecture** – extensible lifecycle-based plugins for optional behavior
60
+ - **Hooks** logging, auth, metrics, request/response transformation
61
+ - **Pending requests** – real-time monitoring of active requests
62
+ - **Per-request overrides** – customize behavior on a per-request basis
63
+ - **Universal** – Node.js, Browser, Cloudflare Workers, React Native
64
+ - **Zero runtime deps** ships as dual ESM/CJS
65
+ - **Configurable error handling** – custom error types and `throwOnHttpError` flag to throw on HTTP errors
66
+ - **Circuit breaker plugin (optional, prebuilt)** – automatic failure protection
67
+ - **Deduplication plugin (optional, prebuilt)** automatic deduping of in-flight identical requests
68
+ - **Request shortcuts plugin (optional, prebuilt)** – call `client.get(url)` / `.post()` / `.put()` / `.patch()` / `.delete()` directly on the client
69
+ - **Response shortcuts plugin (optional, prebuilt)** – call `client(url).json()` / `.text()` / `.blob()` directly on the request promise
70
+
71
+ **Built-in error classes:** `TimeoutError`, `RetryLimitError`, `CircuitOpenError`, `HttpError`, `NetworkError`, `AbortError`
72
+
73
+ ### Built-in Plugins at a Glance
74
+
75
+ All plugins are tree-shakeable import only what you use.
76
+
77
+ - **dedupePlugin (optional)**: dedupe in-flight identical requests.
78
+ - **circuitPlugin (optional)**: fail fast after repeated failures.
79
+ - **requestShortcutsPlugin (optional)**: HTTP method shortcuts on the client (`.get()` / `.post()` / `.put()` / `.patch()` / `.delete()` / `.head()` / `.options()`).
80
+ - **responseShortcutsPlugin (optional)**: use `client(url).json()` / `.text()` / `.blob()` style parsing.
81
+
82
+ ## What Problems Does ffetch Solve?
83
+
84
+ ffetch is ideal for:
85
+
86
+ - **Microservices and REST APIs** with retry requirements and timeout control
87
+ - **High-traffic client applications** that need in-flight deduplication and circuit breaker protection
88
+ - **SSR and metaframework apps** that require runtime flexibility (custom fetch handlers for different environments)
89
+ - **Type-safe request handling** with strong TypeScript support and zero runtime dependencies
90
+
91
+ ## Quick Start
92
+
93
+ ### Install
94
+
95
+ ```bash
96
+ # npm
97
+ npm install @fetchkit/ffetch
98
+
99
+ # yarn
100
+ yarn add @fetchkit/ffetch
101
+
102
+ # pnpm
103
+ pnpm add @fetchkit/ffetch
104
+
105
+ # bun
106
+ bun add @fetchkit/ffetch
107
+ ```
108
+
109
+ ### Basic Setup
110
+
111
+ ```typescript
112
+ import { createClient } from '@fetchkit/ffetch'
113
+
114
+ type User = { id: number; name: string }
115
+
116
+ const api = createClient({ timeout: 5000, retries: 2 })
117
+ const response = await api('https://api.example.com/users')
118
+
119
+ if (!response.ok) {
120
+ throw new Error(`Request failed: ${response.status}`)
121
+ }
122
+
123
+ const users = (await response.json()) as User[]
124
+ ```
125
+
126
+ ### Production Setup with Plugins
127
+
128
+ ```typescript
129
+ import { createClient } from '@fetchkit/ffetch'
130
+ import { dedupePlugin } from '@fetchkit/ffetch/plugins/dedupe'
131
+ import { circuitPlugin } from '@fetchkit/ffetch/plugins/circuit'
132
+ import { requestShortcutsPlugin } from '@fetchkit/ffetch/plugins/request-shortcuts'
133
+ import { responseShortcutsPlugin } from '@fetchkit/ffetch/plugins/response-shortcuts'
134
+
135
+ const api = createClient({
136
+ timeout: 10_000,
137
+ retries: 2,
138
+ plugins: [
139
+ // 1) Optional: dedupe identical in-flight requests
140
+ dedupePlugin({ ttl: 30_000, sweepInterval: 5_000 }),
141
+ // 2) Optional: open the circuit after repeated failures
142
+ circuitPlugin({ threshold: 5, reset: 30_000 }),
143
+ // 3) Optional: enable request-promise parsing shortcuts
144
+ responseShortcutsPlugin(),
145
+ // 4) Optional: enable client HTTP method shortcuts
146
+ requestShortcutsPlugin(),
147
+ ],
148
+ })
149
+
150
+ const users = await api
151
+ .get('https://api.example.com/users')
152
+ .json<Array<{ id: number; name: string }>>()
153
+
154
+ const p1 = api('https://api.example.com/data')
155
+ const p2 = api('https://api.example.com/data')
156
+ const [res1, res2] = await Promise.all([p1, p2])
157
+ ```
158
+
159
+ What this setup gives you:
160
+
161
+ - **Operational safety**: retries with timeout defaults.
162
+ - **Lower duplicate traffic (optional)**: concurrent identical requests share one in-flight call.
163
+ - **Faster failure recovery (optional)**: circuit breaker blocks repeated failing calls.
164
+ - **Cleaner request ergonomics (optional)**: `client.get(url)` / `.post(url, init)` style shortcuts.
165
+ - **Cleaner parsing (optional)**: `client(url).json()` style shortcuts.
166
+
167
+ ### Why not only native fetch?
168
+
169
+ - Native fetch is a great baseline, but production apps usually need retries and timeout control.
170
+ - ffetch keeps the fetch model and adds optional resilience features.
171
+ - You can keep strict native behavior and only opt into plugins you need.
172
+
173
+ ### Common Recipes
174
+
175
+ ```typescript
176
+ // Throw on non-2xx/429 once retries are exhausted
177
+ const strict = createClient({ throwOnHttpError: true })
178
+
179
+ // Use a custom fetch implementation (SSR/framework/runtime)
180
+ import nodeFetch from 'node-fetch'
181
+ const apiWithCustomHandler = createClient({ fetchHandler: nodeFetch })
182
+
183
+ // Keep native Response flow (works with or without plugins)
184
+ const plainApi = createClient({ timeout: 5000 })
185
+ const response = await plainApi('https://api.example.com/health')
186
+ const text = await response.text()
187
+ ```
188
+
189
+ ### Using a Custom fetchHandler (SSR, metaframeworks, or polyfills)
190
+
191
+ ```typescript
192
+ // Why this exists:
193
+ // ffetch wraps whatever fetch-compatible function you provide.
194
+ // This is useful when your runtime has a scoped/framework fetch,
195
+ // or when Node needs an explicit fetch implementation.
196
+
197
+ import { createClient } from '@fetchkit/ffetch'
198
+ import nodeFetch from 'node-fetch'
199
+
200
+ // Node.js example: provide node-fetch explicitly
201
+ const apiNode = createClient({
202
+ fetchHandler: nodeFetch,
203
+ timeout: 5000,
204
+ })
205
+ const nodeResponse = await apiNode('https://api.example.com/data')
206
+
207
+ // Framework example: pass the framework-scoped fetch
208
+ // (e.g. the fetch passed into a request handler)
209
+ async function loadData(frameworkFetch: typeof fetch) {
210
+ const api = createClient({
211
+ fetchHandler: frameworkFetch,
212
+ timeout: 5000,
213
+ })
214
+
215
+ const response = await api('/internal/data')
216
+ return response.json()
217
+ }
218
+ ```
219
+
220
+ All ffetch features (timeouts, retries, plugins, hooks) behave the same with a custom `fetchHandler`.
221
+
222
+ With `responseShortcutsPlugin()` enabled, request-promise shortcuts like `api(url).json()` also work the same.
223
+
224
+ ### Advanced Example
225
+
226
+ ```typescript
227
+ // Production-ready client with error handling and monitoring
228
+ import { createClient } from '@fetchkit/ffetch'
229
+ import { dedupePlugin } from '@fetchkit/ffetch/plugins/dedupe'
230
+ import { circuitPlugin } from '@fetchkit/ffetch/plugins/circuit'
231
+
232
+ const client = createClient({
233
+ timeout: 10000,
234
+ retries: 2,
235
+ fetchHandler: fetch, // Use custom fetch if needed
236
+ plugins: [
237
+ dedupePlugin({
238
+ hashFn: (params) => `${params.method}|${params.url}|${params.body}`,
239
+ ttl: 30_000,
240
+ sweepInterval: 5_000,
241
+ }),
242
+ circuitPlugin({
243
+ threshold: 5,
244
+ reset: 30_000,
245
+ onCircuitOpen: (req) => console.warn('Circuit opened due to:', req.url),
246
+ onCircuitClose: (req) => console.info('Circuit closed after:', req.url),
247
+ }),
248
+ ],
249
+ hooks: {
250
+ before: async (req) => console.log('→', req.url),
251
+ after: async (req, res) => console.log('←', res.status),
252
+ onError: async (req, err) => console.error('Error:', err.message),
253
+ },
254
+ })
255
+
256
+ try {
257
+ const response = await client('/api/data')
258
+
259
+ // Check HTTP status manually (like native fetch)
260
+ if (!response.ok) {
261
+ console.log('HTTP error:', response.status)
262
+ return
263
+ }
264
+
265
+ const data = await response.json()
266
+ console.log('Active requests:', client.pendingRequests.length)
267
+ } catch (err) {
268
+ if (err instanceof TimeoutError) {
269
+ console.log('Request timed out')
270
+ } else if (err instanceof RetryLimitError) {
271
+ console.log('Request failed after retries')
272
+ }
273
+ }
274
+ ```
275
+
276
+ ### Custom Error Handling with `throwOnHttpError`
277
+
278
+ Native `fetch`'s controversial behavior of not throwing errors for HTTP error status codes (4xx, 5xx) can lead to overlooked errors in applications. By default, `ffetch` follows this same pattern, returning a `Response` object regardless of the HTTP status code. However, with the `throwOnHttpError` flag, developers can configure `ffetch` to throw an `HttpError` for HTTP error responses, making error handling more explicit and robust. Note that this behavior is affected by retries and the circuit breaker - full details are explained in the [Error Handling documentation](./docs/errorhandling.md).
279
+
280
+ ## Documentation
281
+
282
+ | Topic | Description |
283
+ | --------------------------------------------- | ------------------------------------------------------------------------- |
284
+ | **[Complete Documentation](./docs/index.md)** | **Start here** - Documentation index and overview |
285
+ | **[API Reference](./docs/api.md)** | Complete API documentation and configuration options |
286
+ | **[Plugin Architecture](./docs/plugins.md)** | Plugin lifecycle, custom plugin authoring, and integration patterns |
287
+ | **[Deduplication](./docs/deduplication.md)** | How deduplication works, hash config, optional TTL cleanup, limitations |
288
+ | **[Error Handling](./docs/errorhandling.md)** | Strategies for managing errors, including `throwOnHttpError` |
289
+ | **[Advanced Features](./docs/advanced.md)** | Per-request overrides, pending requests, circuit breakers, custom errors |
290
+ | **[Hooks & Transformation](./docs/hooks.md)** | Lifecycle hooks, authentication, logging, request/response transformation |
291
+ | **[Usage Examples](./docs/examples.md)** | Real-world patterns: REST clients, GraphQL, file uploads, microservices |
292
+ | **[Compatibility](./docs/compatibility.md)** | Browser/Node.js support, polyfills, framework integration |
293
+
294
+ ## Environment Requirements
295
+
296
+ `ffetch` works best with native `AbortSignal.any` support:
297
+
298
+ - **Node.js 20.6+** (native `AbortSignal.any`)
299
+ - **Modern browsers with `AbortSignal.any`** (for example: Chrome 117+, Firefox 117+, Safari 17+, Edge 117+)
300
+
301
+ If your environment does not support `AbortSignal.any` (Node.js < 20.6, older browsers), you can still use ffetch by installing an `AbortSignal.any` polyfill. `AbortSignal.timeout` is optional because ffetch includes an internal timeout fallback. See the [compatibility guide](./docs/compatibility.md) for instructions.
302
+
303
+ **Custom fetch support:**
304
+ You can pass any fetch-compatible implementation (native fetch, node-fetch, undici, SvelteKit, Next.js, Nuxt, or a polyfill) via the `fetchHandler` option. This makes ffetch fully compatible with SSR, edge, metaframework environments, custom backends, and test runners.
305
+
306
+ #### "AbortSignal.any is not a function"
307
+
308
+ Solution: Install a polyfill for `AbortSignal.any`
309
+
310
+ ```bash
311
+ npm install abort-controller-x
312
+ ```
313
+
314
+ ## CDN Usage
315
+
316
+ ```html
317
+ <script type="module">
318
+ import { createClient } from 'https://unpkg.com/@fetchkit/ffetch/dist/index.min.js'
319
+
320
+ const api = createClient({ timeout: 5000 })
321
+ const data = await api('/api/data').then((r) => r.json())
322
+ </script>
323
+ ```
324
+
325
+ ## Deduplication Limitations
326
+
327
+ - Deduplication is **off** by default. Enable it via `plugins: [dedupePlugin()]`.
328
+ - The default hash function is `dedupeRequestHash`, which handles common body types and skips deduplication for streams and FormData.
329
+ - Optional stale-entry cleanup: `dedupePlugin({ ttl, sweepInterval })` enables map-entry eviction. TTL eviction only removes dedupe keys; it does not reject already in-flight promises.
330
+ - **Stream bodies** (`ReadableStream`, `FormData`): Deduplication is skipped for requests with these body types, as they cannot be reliably hashed or replayed.
331
+ - **Non-idempotent requests**: Use deduplication with caution for non-idempotent methods (e.g., POST), as it may suppress multiple intended requests.
332
+ - **Custom hash function**: Ensure your hash function uniquely identifies requests to avoid accidental deduplication.
333
+
334
+ See [deduplication.md](./docs/deduplication.md) for full details.
335
+
336
+ ## Fetch vs. Axios vs. ky vs. `ffetch`
337
+
338
+ | Feature | Native Fetch | Axios | ky | ffetch |
339
+ | -------------------- | ------------------------------------------------------- | ------------------------------ | --------------------------------------------- | -------------------------------------------------------------------------------------- |
340
+ | Timeouts | ❌ Manual AbortController | ✅ Built-in | ✅ Built-in | ✅ Built-in with fallbacks |
341
+ | Retries | ❌ Manual implementation | ❌ Manual or plugins | ✅ Built-in | ✅ Smart exponential backoff |
342
+ | Response Parsing DX | ⚠️ Response methods only (`await fetch(...).then(...)`) | ✅ `response.data` convenience | ✅ `.json()/.text()/.blob()` on request chain | ✅ Optional `responseShortcutsPlugin()` (`.json()/.text()/.blob()` on request chain) |
343
+ | Plugin Architecture | ❌ Not available | ⚠️ Interceptors only | ⚠️ Hook-based extensions | ✅ First-class plugin pipeline (optional built-in + custom plugins) |
344
+ | Circuit Breaker | ❌ Not available | ❌ Manual or plugins | ❌ Manual | ✅ Automatic failure protection |
345
+ | Deduplication | ❌ Not available | ❌ Not available | ❌ Not available | ✅ Optional via `dedupePlugin()` |
346
+ | Request Monitoring | ❌ Manual tracking | ❌ Manual tracking | ❌ Manual tracking | ✅ Built-in pending requests |
347
+ | Error Types | ❌ Generic errors | ⚠️ HTTP errors only | ✅ Specific error classes | ✅ Specific error classes |
348
+ | TypeScript | ⚠️ Basic types | ⚠️ Basic types | ✅ Strong types | ✅ Full type safety |
349
+ | Hooks/Middleware | ❌ Not available | ✅ Interceptors | ✅ Hooks | ✅ Comprehensive lifecycle hooks |
350
+ | Bundle Size | ✅ Native (0kb) | ❌ ~13kb minified | ✅ Lightweight (fetch-based) | ✅ ~3kb minified |
351
+ | Modern APIs | ✅ Web standards | ❌ XMLHttpRequest | ✅ Fetch + modern APIs | ✅ Fetch + modern features |
352
+ | Custom Fetch Support | ❌ No (global only) | ❌ No | ❌ No | ✅ Yes (wrap any fetch-compatible implementation, including framework or custom fetch) |
353
+
354
+ Note: built-in plugins in ffetch are opt-in. Use `dedupePlugin()` for deduplication, `circuitPlugin()` for circuit breaking, `requestShortcutsPlugin()` for client HTTP method shortcuts, and `responseShortcutsPlugin()` for request-promise parsing shortcuts. Bundle size: ~3kb core, additional optional plugin imports are tree-shakeable.
355
+
356
+ ### Try ffetch in Action
357
+
358
+ Want to see these clients in practice? Check out [ffetch-demo](https://github.com/fetch-kit/ffetch-demo) for working examples and side-by-side comparisons of how ffetch simplifies common fetch patterns.
359
+
360
+ ## Join the Community
361
+
362
+ Got questions, want to discuss features, or share examples? Join the **Fetch-Kit Discord server**:
363
+
364
+ [![Discord](https://img.shields.io/badge/Discord-Join_Fetch--Kit-7289DA?logo=discord&logoColor=white)](https://discord.gg/sdyPBPCDUg)
365
+
366
+ ## Contributing
367
+
368
+ - **Issues**: [GitHub Issues](https://github.com/fetch-kit/ffetch/issues)
369
+ - **Pull Requests**: [GitHub PRs](https://github.com/fetch-kit/ffetch/pulls)
370
+ - **Documentation**: Found in `./docs/` - PRs welcome!
371
+
372
+ ## License
373
+
374
+ MIT © 2025 gkoos