@scirexs/fetchy 0.5.0 → 0.7.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 +439 -201
- package/esm/main.js +1 -1
- package/esm/mod.js +1 -1
- package/package.json +7 -1
- package/types/main.d.ts +262 -132
- package/types/main.d.ts.map +1 -1
- package/types/mod.d.ts +2 -2
- package/types/mod.d.ts.map +1 -1
- package/types/types.d.ts +118 -25
- package/types/types.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -4,21 +4,22 @@
|
|
|
4
4
|
[](https://jsr.io/@scirexs/fetchy)
|
|
5
5
|
[](https://github.com/scirexs/fetchy/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
A lightweight, type-safe fetch wrapper with built-in retry logic, timeout handling, and automatic body parsing.
|
|
7
|
+
A lightweight, type-safe fetch wrapper with built-in retry logic, timeout handling, and automatic body parsing. Works in Deno, Node.js, and modern browsers.
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- **Lightweight** -
|
|
11
|
+
- **Lightweight** - Bundle size is ~5KB uncompressed, ~2KB gzipped, zero dependencies
|
|
12
12
|
- **Simple API** - Drop-in replacement for native fetch with enhanced capabilities
|
|
13
|
+
- **Promise-like Interface** - Chain parsing methods directly on fetch results
|
|
13
14
|
- **Timeout Support** - Configurable request timeouts with automatic cancellation
|
|
14
15
|
- **Retry Logic** - Exponential backoff with Retry-After header support
|
|
15
16
|
- **Type-Safe** - Full TypeScript support with generic type inference
|
|
16
17
|
- **Bearer Token Helper** - Built-in Authorization header management
|
|
17
18
|
- **Jitter Support** - Prevent thundering herd with randomized delays
|
|
18
|
-
- **
|
|
19
|
+
- **Fluent Interface** - Class-based API with both instance and static methods
|
|
20
|
+
- **HTTP Method Shortcuts** - Convenient methods for GET, POST, PUT, PATCH, DELETE
|
|
19
21
|
|
|
20
22
|
## Installation
|
|
21
|
-
|
|
22
23
|
```bash
|
|
23
24
|
# npm
|
|
24
25
|
npm install @scirexs/fetchy
|
|
@@ -28,151 +29,246 @@ deno add jsr:@scirexs/fetchy
|
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
## Quick Start
|
|
31
|
-
|
|
32
32
|
```ts
|
|
33
|
-
import { fetchy,
|
|
33
|
+
import { fetchy, sfetchy, Fetchy, fy } from "@scirexs/fetchy";
|
|
34
34
|
|
|
35
|
-
// Simple GET request with
|
|
36
|
-
const
|
|
37
|
-
timeout: 10
|
|
38
|
-
});
|
|
35
|
+
// Simple GET request with automatic JSON parsing
|
|
36
|
+
const user = await fetchy("https://api.example.com/user/1").json<User>();
|
|
39
37
|
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
// Safe error handling - returns null on failure
|
|
39
|
+
const data = await sfetchy("https://api.example.com/data").json<Data>();
|
|
40
|
+
if (data !== null) {
|
|
41
|
+
console.log(data);
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
// Fluent API with reusable configuration
|
|
45
|
+
const client = fy({
|
|
46
|
+
bearer: "token",
|
|
47
|
+
timeout: 10,
|
|
48
|
+
retry: { maxAttempts: 5 }
|
|
49
|
+
});
|
|
50
|
+
const posts = await client.get("/posts").json<Post[]>();
|
|
48
51
|
```
|
|
49
52
|
|
|
50
53
|
## API Reference
|
|
51
54
|
|
|
52
|
-
### `fetchy(url
|
|
55
|
+
### `fetchy(url?, options?)`
|
|
53
56
|
|
|
54
|
-
Performs an HTTP request
|
|
57
|
+
Performs an HTTP request with enhanced features. Returns a promise-like object that can be awaited directly or chained with parsing methods.
|
|
55
58
|
|
|
56
59
|
#### Parameters
|
|
57
60
|
|
|
58
|
-
- `url`: `string | URL | Request | null` - The request URL
|
|
61
|
+
- `url`: `string | URL | Request | null` - The request URL (can be null if `options.url` is provided)
|
|
59
62
|
- `options`: `FetchyOptions` (optional) - Configuration options
|
|
60
63
|
|
|
61
64
|
#### Returns
|
|
62
65
|
|
|
63
|
-
`
|
|
66
|
+
`FetchyResponse` - A promise-like object that extends `Promise<Response>` with the following methods:
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
- `text()` → `Promise<string>` - Parse response as text
|
|
69
|
+
- `json<T>()` → `Promise<T>` - Parse response as JSON
|
|
70
|
+
- `bytes()` → `Promise<Uint8Array>` - Parse response as byte array
|
|
71
|
+
- `blob()` → `Promise<Blob>` - Parse response as Blob
|
|
72
|
+
- `arrayBuffer()` → `Promise<ArrayBuffer>` - Parse response as ArrayBuffer
|
|
73
|
+
- `formData()` → `Promise<FormData>` - Parse response as FormData
|
|
66
74
|
|
|
75
|
+
#### Example
|
|
67
76
|
```ts
|
|
68
|
-
|
|
77
|
+
// Get Response object
|
|
78
|
+
const response = await fetchy("https://api.example.com/data");
|
|
79
|
+
|
|
80
|
+
// Chain JSON parsing
|
|
81
|
+
const user = await fetchy("https://api.example.com/user").json<User>();
|
|
82
|
+
|
|
83
|
+
// POST with automatic body serialization
|
|
84
|
+
const result = await fetchy("https://api.example.com/create", {
|
|
69
85
|
method: "POST",
|
|
70
|
-
body: {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
bearer: "your-token-here"
|
|
74
|
-
});
|
|
86
|
+
body: { name: "John", age: 30 },
|
|
87
|
+
bearer: "token"
|
|
88
|
+
}).json();
|
|
75
89
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
90
|
+
// Binary data
|
|
91
|
+
const image = await fetchy("https://api.example.com/image.png").bytes();
|
|
79
92
|
```
|
|
80
93
|
|
|
81
|
-
### `
|
|
94
|
+
### `sfetchy(url?, options?)`
|
|
82
95
|
|
|
83
|
-
Performs an HTTP request
|
|
96
|
+
Performs an HTTP request with safe error handling. Returns `null` on any failure instead of throwing.
|
|
84
97
|
|
|
85
98
|
#### Parameters
|
|
86
99
|
|
|
87
|
-
|
|
88
|
-
- `type`: `"text" | "json" | "bytes" | "auto"` (default: `"auto"`) - Response parsing type
|
|
89
|
-
- `options`: `FetchyOptions` (optional) - Configuration options
|
|
100
|
+
Same as `fetchy()`.
|
|
90
101
|
|
|
91
102
|
#### Returns
|
|
92
103
|
|
|
93
|
-
`
|
|
104
|
+
`FetchySafeResponse` - A promise-like object that extends `Promise<Response | null>` with the same parsing methods as `FetchyResponse`.
|
|
94
105
|
|
|
95
|
-
|
|
106
|
+
- `text()` → `Promise<string | null>` - Safe text parsing (returns null on error)
|
|
107
|
+
- `json<T>()` → `Promise<T | null>` - Safe JSON parsing (returns null on error)
|
|
108
|
+
- `bytes()` → `Promise<Uint8Array | null>` - Safe bytes parsing
|
|
109
|
+
- `blob()` → `Promise<Blob | null>` - Safe blob parsing
|
|
110
|
+
- `arrayBuffer()` → `Promise<ArrayBuffer | null>` - Safe buffer parsing
|
|
111
|
+
- `formData()` → `Promise<FormData | null>` - Safe form data parsing
|
|
96
112
|
|
|
113
|
+
#### Example
|
|
97
114
|
```ts
|
|
98
|
-
//
|
|
99
|
-
const
|
|
115
|
+
// Returns null instead of throwing on error
|
|
116
|
+
const response = await sfetchy("https://api.example.com/data");
|
|
117
|
+
if (response === null) {
|
|
118
|
+
console.log("Request failed gracefully");
|
|
119
|
+
} else {
|
|
120
|
+
const data = await response.json();
|
|
121
|
+
}
|
|
100
122
|
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
price: number;
|
|
123
|
+
// Safe parsing still available
|
|
124
|
+
const data = await sfetchy("https://api.example.com/data").json<Data>();
|
|
125
|
+
if (data !== null) {
|
|
126
|
+
// Handle successful response
|
|
106
127
|
}
|
|
107
|
-
|
|
128
|
+
```
|
|
108
129
|
|
|
109
|
-
|
|
110
|
-
const html = await fetchyb("https://example.com", "text");
|
|
130
|
+
### `Fetchy` Class
|
|
111
131
|
|
|
112
|
-
|
|
113
|
-
|
|
132
|
+
A fluent HTTP client class that provides instance methods.
|
|
133
|
+
|
|
134
|
+
#### Constructor
|
|
135
|
+
```ts
|
|
136
|
+
const client = new Fetchy(options?: FetchyOptions);
|
|
114
137
|
```
|
|
115
138
|
|
|
116
|
-
|
|
139
|
+
#### Instance Methods
|
|
140
|
+
```ts
|
|
141
|
+
const client = new Fetchy(options);
|
|
142
|
+
|
|
143
|
+
// Main fetch method
|
|
144
|
+
await client.fetch(url?) // Returns FetchyResponse
|
|
145
|
+
|
|
146
|
+
// HTTP method shortcuts
|
|
147
|
+
await client.get(url?) // GET request, returns FetchyResponse
|
|
148
|
+
await client.post(url?) // POST request, returns FetchyResponse
|
|
149
|
+
await client.put(url?) // PUT request, returns FetchyResponse
|
|
150
|
+
await client.patch(url?) // PATCH request, returns FetchyResponse
|
|
151
|
+
await client.delete(url?) // DELETE request, returns FetchyResponse
|
|
152
|
+
await client.head(url?) // HEAD request, returns Promise<Response>
|
|
153
|
+
|
|
154
|
+
// Safe mode methods
|
|
155
|
+
await client.safe(url?) // Returns FetchySafeResponse
|
|
156
|
+
await client.sget(url?) // Safe GET, returns FetchySafeResponse
|
|
157
|
+
await client.spost(url?) // Safe POST, returns FetchySafeResponse
|
|
158
|
+
await client.sput(url?) // Safe PUT, returns FetchySafeResponse
|
|
159
|
+
await client.spatch(url?) // Safe PATCH, returns FetchySafeResponse
|
|
160
|
+
await client.sdelete(url?) // Safe DELETE, returns FetchySafeResponse
|
|
161
|
+
await client.shead(url?) // Safe HEAD, returns Promise<Response | null>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
All methods can be chained with parsing methods:
|
|
165
|
+
```ts
|
|
166
|
+
await client.get("/users").json<User[]>();
|
|
167
|
+
await client.post("/create").json<Result>();
|
|
168
|
+
await client.safe("/data").text();
|
|
169
|
+
```
|
|
117
170
|
|
|
118
|
-
|
|
171
|
+
#### Example
|
|
172
|
+
```ts
|
|
173
|
+
// Instance usage - reuse configuration
|
|
174
|
+
const client = new Fetchy({
|
|
175
|
+
base: "https://api.example.com",
|
|
176
|
+
bearer: "token123",
|
|
177
|
+
timeout: 10,
|
|
178
|
+
retry: { maxAttempts: 3 }
|
|
179
|
+
});
|
|
119
180
|
|
|
120
|
-
|
|
181
|
+
const user = await client.get("/user").json<User>();
|
|
182
|
+
const posts = await client.get("/posts").json<Post[]>();
|
|
121
183
|
|
|
184
|
+
// POST with body
|
|
185
|
+
const result = await client.post("/create", {
|
|
186
|
+
body: { name: "John" }
|
|
187
|
+
}).json();
|
|
188
|
+
|
|
189
|
+
// Safe mode
|
|
190
|
+
const data = await client.sget("/data").json<Data>();
|
|
191
|
+
if (data !== null) {
|
|
192
|
+
// Handle successful response
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `fy(options?)` Function
|
|
197
|
+
|
|
198
|
+
Shorthand for creating a new `Fetchy` instance.
|
|
199
|
+
```ts
|
|
200
|
+
const client = fy({
|
|
201
|
+
base: "https://api.example.com",
|
|
202
|
+
bearer: "token"
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const user = await client.get("/user").json<User>();
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Equivalent to:
|
|
122
209
|
```ts
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
210
|
+
const client = new Fetchy({
|
|
211
|
+
base: "https://api.example.com",
|
|
212
|
+
bearer: "token"
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Configuration
|
|
217
|
+
|
|
218
|
+
### `FetchyOptions`
|
|
219
|
+
```ts
|
|
220
|
+
interface FetchyOptions extends Omit<RequestInit, "body"> {
|
|
221
|
+
// Request URL (allows null url parameter with this option)
|
|
222
|
+
url?: string | URL | Request;
|
|
223
|
+
|
|
224
|
+
// Base URL prepended to request URL (only for string/URL, not Request)
|
|
225
|
+
base?: string | URL;
|
|
127
226
|
|
|
128
|
-
// Request body (auto-serializes JSON
|
|
129
|
-
|
|
130
|
-
body?: JSONValue | FormData | URLSearchParams | Blob | ArrayBuffer | string;
|
|
227
|
+
// Request body (auto-serializes JSON objects)
|
|
228
|
+
body?: JSONValue | BodyInit;
|
|
131
229
|
|
|
132
230
|
// Timeout in seconds (default: 15)
|
|
133
|
-
timeout?: number;
|
|
231
|
+
timeout?: number;
|
|
134
232
|
|
|
135
|
-
// Retry configuration
|
|
136
|
-
retry?:
|
|
137
|
-
maxAttempts?: number; // Maximum retry attempts (default: 3)
|
|
138
|
-
interval?: number; // Base interval in seconds (default: 3)
|
|
139
|
-
maxInterval?: number; // Maximum interval cap in seconds (default: 30)
|
|
140
|
-
retryAfter?: boolean; // Respect Retry-After header (default: true)
|
|
141
|
-
} | false; // Set to false to disable retry
|
|
233
|
+
// Retry configuration (set to false to disable)
|
|
234
|
+
retry?: RetryOptions | false;
|
|
142
235
|
|
|
143
236
|
// Bearer token (automatically adds "Bearer " prefix)
|
|
144
237
|
bearer?: string;
|
|
145
238
|
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
onNative?: boolean; // Throw on native errors (default: true)
|
|
149
|
-
onStatus?: boolean; // Throw on 4xx/5xx status (default: true)
|
|
150
|
-
} | boolean; // Set to true to throw on all errors
|
|
239
|
+
// Maximum jitter delay in seconds before request (default: 0)
|
|
240
|
+
jitter?: number;
|
|
151
241
|
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// URL for fetch (allows reusing FetchyOptions as preset configuration)
|
|
156
|
-
url?: string | URL;
|
|
242
|
+
// Use native fetch error behavior (no HTTPStatusError on 4xx/5xx)
|
|
243
|
+
native?: boolean;
|
|
157
244
|
}
|
|
245
|
+
|
|
246
|
+
interface RetryOptions {
|
|
247
|
+
maxAttempts?: number; // Maximum retry attempts (default: 3)
|
|
248
|
+
interval?: number; // Base interval in seconds (default: 3)
|
|
249
|
+
maxInterval?: number; // Maximum interval cap (default: 30)
|
|
250
|
+
retryOnTimeout?: boolean; // Retry on timeout (default: true)
|
|
251
|
+
idempotentOnly?: boolean; // Only retry idempotent methods (default: false)
|
|
252
|
+
statusCodes?: number[]; // Status codes to retry (default: [500, 502, 503, 504, 408, 429])
|
|
253
|
+
respectHeaders?: string[]; // Headers to respect for retry timing
|
|
254
|
+
} // (default: ["retry-after", "ratelimit-reset", "x-ratelimit-reset"])
|
|
158
255
|
```
|
|
159
256
|
|
|
160
257
|
#### Default Values
|
|
161
|
-
|
|
162
258
|
```ts
|
|
163
259
|
{
|
|
164
260
|
timeout: 15, // 15 seconds
|
|
165
|
-
|
|
261
|
+
jitter: 0, // No jitter delay
|
|
262
|
+
native: false, // Throws HTTPStatusError on non-OK status
|
|
166
263
|
retry: {
|
|
167
264
|
maxAttempts: 3, // 3 retry attempts
|
|
168
265
|
interval: 3, // 3 seconds base interval
|
|
169
266
|
maxInterval: 30, // 30 seconds maximum interval
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
},
|
|
267
|
+
retryOnTimeout: true, // Retry on timeout
|
|
268
|
+
idempotentOnly: false, // Retry all methods
|
|
269
|
+
statusCodes: [500, 502, 503, 504, 408, 429],
|
|
270
|
+
respectHeaders: ["retry-after", "ratelimit-reset", "x-ratelimit-reset"]
|
|
271
|
+
}
|
|
176
272
|
}
|
|
177
273
|
```
|
|
178
274
|
|
|
@@ -180,88 +276,118 @@ interface FetchyOptions extends RequestInit {
|
|
|
180
276
|
|
|
181
277
|
#### Method
|
|
182
278
|
|
|
183
|
-
If
|
|
279
|
+
- If body is provided without method: defaults to `"POST"`
|
|
280
|
+
- If Request object is passed: uses its method
|
|
281
|
+
- Otherwise: defaults to `"GET"`
|
|
184
282
|
|
|
185
283
|
#### Headers
|
|
186
284
|
|
|
187
285
|
The following headers are automatically set if not specified:
|
|
188
286
|
|
|
189
287
|
- **Accept**: `application/json, text/plain`
|
|
190
|
-
- **Content-Type**: Automatically determined based on
|
|
191
|
-
- `string`, `URLSearchParams`, `FormData`, `Blob` with type: Not set
|
|
192
|
-
- `JSONValue
|
|
193
|
-
- `Blob` without type
|
|
194
|
-
|
|
195
|
-
- **Authorization**: Set to `Bearer ${options.bearer}` if `options.bearer` is provided.
|
|
196
|
-
|
|
197
|
-
**Note 1:** If you pass serialized JSON as the body (i.e., a string), Content-Type will be set to `text/plain;charset=UTF-8`. To ensure Content-Type is set to `application/json`, pass the JSON object directly instead of a serialized string.
|
|
288
|
+
- **Content-Type**: Automatically determined based on body type:
|
|
289
|
+
- `string`, `URLSearchParams`, `FormData`, `Blob` with type: Not set (native fetch handles it)
|
|
290
|
+
- `JSONValue` (objects, arrays, numbers, booleans): `application/json`
|
|
291
|
+
- `Blob` without type, `ArrayBuffer`: `application/octet-stream`
|
|
292
|
+
- **Authorization**: `Bearer ${options.bearer}` if bearer is provided
|
|
198
293
|
|
|
199
|
-
**Note
|
|
294
|
+
**Note:** Headers from Request objects are preserved and merged with option headers.
|
|
200
295
|
|
|
201
296
|
## Error Handling
|
|
202
297
|
|
|
203
|
-
###
|
|
298
|
+
### HTTPStatusError
|
|
204
299
|
|
|
205
|
-
|
|
300
|
+
Thrown when response status is not OK (4xx, 5xx) unless `native: true` is set.
|
|
301
|
+
```ts
|
|
302
|
+
import { fetchy, HTTPStatusError } from "@scirexs/fetchy";
|
|
206
303
|
|
|
207
|
-
|
|
304
|
+
try {
|
|
305
|
+
await fetchy("https://api.example.com/data");
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (error instanceof HTTPStatusError) {
|
|
308
|
+
console.error(error.status); // 404
|
|
309
|
+
console.error(error.response); // Response object
|
|
310
|
+
console.error(error.message); // "404 https://api.example.com/data"
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
208
314
|
|
|
209
|
-
|
|
315
|
+
### Native Errors
|
|
210
316
|
|
|
211
|
-
|
|
317
|
+
Other errors (network failures, timeout, abort) are thrown as standard errors:
|
|
318
|
+
- `TypeError`: Network error, DNS resolution failure
|
|
319
|
+
- `DOMException`: Timeout or abort via AbortSignal
|
|
212
320
|
|
|
213
|
-
|
|
321
|
+
### Safe Error Handling
|
|
214
322
|
|
|
215
|
-
|
|
323
|
+
Use `sfetchy()` or safe methods to return `null` instead of throwing:
|
|
324
|
+
```ts
|
|
325
|
+
// Safe fetch - returns null on any error
|
|
326
|
+
const response = await sfetchy("https://api.example.com/data");
|
|
327
|
+
if (response === null) {
|
|
328
|
+
// Handle error gracefully
|
|
329
|
+
}
|
|
216
330
|
|
|
217
|
-
|
|
331
|
+
// Safe parsing methods - return null on error
|
|
332
|
+
const data = await fetchy("https://api.example.com/data").json<Data>();
|
|
333
|
+
if (data !== null) {
|
|
334
|
+
// Process data
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Native Mode
|
|
339
|
+
|
|
340
|
+
Set `native: true` to disable HTTPStatusError and get native fetch behavior:
|
|
341
|
+
```ts
|
|
342
|
+
const response = await fetchy("https://api.example.com/data", {
|
|
343
|
+
native: true
|
|
344
|
+
});
|
|
345
|
+
// Returns Response even for 4xx/5xx status codes
|
|
346
|
+
if (!response.ok) {
|
|
347
|
+
console.error("Request failed");
|
|
348
|
+
}
|
|
349
|
+
```
|
|
218
350
|
|
|
219
351
|
## Usage Examples
|
|
220
352
|
|
|
221
353
|
### Basic Requests
|
|
222
|
-
|
|
223
354
|
```ts
|
|
224
|
-
import { fetchy,
|
|
355
|
+
import { fetchy, sfetchy } from "@scirexs/fetchy";
|
|
225
356
|
|
|
226
|
-
// GET
|
|
227
|
-
const
|
|
357
|
+
// GET with automatic JSON parsing
|
|
358
|
+
const users = await fetchy("https://api.example.com/users").json<User[]>();
|
|
228
359
|
|
|
229
360
|
// POST with JSON body
|
|
230
|
-
const result = await
|
|
361
|
+
const result = await fetchy("https://api.example.com/create", {
|
|
362
|
+
method: "POST",
|
|
231
363
|
body: { name: "John", email: "john@example.com" }
|
|
232
|
-
});
|
|
364
|
+
}).json();
|
|
233
365
|
|
|
234
366
|
// Custom headers
|
|
235
367
|
const response = await fetchy("https://api.example.com/data", {
|
|
236
|
-
headers: {
|
|
237
|
-
"X-Custom-Header": "value"
|
|
238
|
-
}
|
|
368
|
+
headers: { "X-Custom-Header": "value" }
|
|
239
369
|
});
|
|
240
370
|
|
|
241
|
-
//
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
371
|
+
// Using base URL
|
|
372
|
+
const data = await fetchy("/users", {
|
|
373
|
+
base: "https://api.example.com"
|
|
374
|
+
}).json();
|
|
245
375
|
```
|
|
246
376
|
|
|
247
377
|
### Authentication
|
|
248
|
-
|
|
249
378
|
```ts
|
|
250
379
|
// Bearer token authentication
|
|
251
|
-
const user = await
|
|
380
|
+
const user = await fetchy("https://api.example.com/me", {
|
|
252
381
|
bearer: "your-access-token"
|
|
253
|
-
});
|
|
382
|
+
}).json<User>();
|
|
254
383
|
|
|
255
384
|
// Custom authorization
|
|
256
|
-
const data = await
|
|
257
|
-
headers: {
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
});
|
|
385
|
+
const data = await fetchy("https://api.example.com/data", {
|
|
386
|
+
headers: { "Authorization": "Basic " + btoa("user:pass") }
|
|
387
|
+
}).json();
|
|
261
388
|
```
|
|
262
389
|
|
|
263
390
|
### Timeout and Retry
|
|
264
|
-
|
|
265
391
|
```ts
|
|
266
392
|
// Custom timeout
|
|
267
393
|
const response = await fetchy("https://slow-api.example.com", {
|
|
@@ -269,15 +395,20 @@ const response = await fetchy("https://slow-api.example.com", {
|
|
|
269
395
|
});
|
|
270
396
|
|
|
271
397
|
// Retry with exponential backoff
|
|
272
|
-
//
|
|
273
|
-
const data = await
|
|
398
|
+
// Intervals: 3s, 6s, 12s, 24s (capped at maxInterval)
|
|
399
|
+
const data = await fetchy("https://api.example.com/data", {
|
|
274
400
|
retry: {
|
|
275
|
-
maxAttempts: 5,
|
|
276
|
-
interval: 3,
|
|
277
|
-
maxInterval: 60
|
|
278
|
-
retryAfter: true // Respect Retry-After header
|
|
401
|
+
maxAttempts: 5,
|
|
402
|
+
interval: 3,
|
|
403
|
+
maxInterval: 60
|
|
279
404
|
}
|
|
280
|
-
});
|
|
405
|
+
}).json();
|
|
406
|
+
|
|
407
|
+
// Retry only idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS, TRACE)
|
|
408
|
+
const result = await fetchy("https://api.example.com/update", {
|
|
409
|
+
method: "POST",
|
|
410
|
+
retry: { idempotentOnly: true } // Won't retry POST
|
|
411
|
+
}).json();
|
|
281
412
|
|
|
282
413
|
// Disable retry
|
|
283
414
|
const response = await fetchy("https://api.example.com/data", {
|
|
@@ -285,94 +416,159 @@ const response = await fetchy("https://api.example.com/data", {
|
|
|
285
416
|
});
|
|
286
417
|
```
|
|
287
418
|
|
|
288
|
-
### Error Handling
|
|
289
|
-
|
|
419
|
+
### Error Handling Patterns
|
|
290
420
|
```ts
|
|
291
|
-
import { fetchy,
|
|
421
|
+
import { fetchy, sfetchy, HTTPStatusError } from "@scirexs/fetchy";
|
|
292
422
|
|
|
293
|
-
//
|
|
423
|
+
// Default: throws on error
|
|
294
424
|
try {
|
|
295
|
-
const
|
|
425
|
+
const data = await fetchy("https://api.example.com/data").json();
|
|
296
426
|
} catch (error) {
|
|
297
427
|
if (error instanceof HTTPStatusError) {
|
|
298
|
-
console.error(
|
|
299
|
-
console.error("Status:", error.status);
|
|
300
|
-
console.error("Body:", error.body);
|
|
428
|
+
console.error(`HTTP ${error.status}:`, error.response);
|
|
301
429
|
}
|
|
302
430
|
}
|
|
303
431
|
|
|
304
|
-
//
|
|
432
|
+
// Safe mode: returns null
|
|
433
|
+
const data = await sfetchy("https://api.example.com/data").json();
|
|
434
|
+
if (data === null) {
|
|
435
|
+
console.log("Request failed, using default");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Native mode: no HTTPStatusError
|
|
305
439
|
const response = await fetchy("https://api.example.com/data", {
|
|
306
|
-
|
|
440
|
+
native: true
|
|
307
441
|
});
|
|
308
|
-
if (response
|
|
309
|
-
console.
|
|
442
|
+
if (!response.ok) {
|
|
443
|
+
console.error("Request failed with status", response.status);
|
|
310
444
|
}
|
|
445
|
+
```
|
|
311
446
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
onError: { onNative: true, onStatus: false } as const
|
|
316
|
-
});
|
|
317
|
-
} catch (error) {
|
|
318
|
-
console.error("Request failed:", error);
|
|
319
|
-
}
|
|
447
|
+
### Fluent API with HTTP Methods
|
|
448
|
+
```ts
|
|
449
|
+
import { Fetchy, fy } from "@scirexs/fetchy";
|
|
320
450
|
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
451
|
+
// Create reusable client
|
|
452
|
+
const api = fy({
|
|
453
|
+
base: "https://api.example.com",
|
|
454
|
+
bearer: "token",
|
|
455
|
+
timeout: 10,
|
|
456
|
+
retry: { maxAttempts: 3 }
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// HTTP method shortcuts
|
|
460
|
+
const users = await api.get("/users").json<User[]>();
|
|
461
|
+
const post = await api.get("/posts/1").json<Post>();
|
|
462
|
+
const created = await api.post("/posts", {
|
|
463
|
+
body: { title: "New Post" }
|
|
464
|
+
}).json();
|
|
465
|
+
const updated = await api.put("/posts/1", {
|
|
466
|
+
body: { title: "Updated" }
|
|
467
|
+
}).json();
|
|
468
|
+
const patched = await api.patch("/posts/1", {
|
|
469
|
+
body: { views: 100 }
|
|
470
|
+
}).json();
|
|
471
|
+
await api.delete("/posts/1");
|
|
472
|
+
|
|
473
|
+
// Safe methods
|
|
474
|
+
const data = await api.sget("/maybe-fails").json();
|
|
475
|
+
if (data !== null) {
|
|
476
|
+
// Process data
|
|
331
477
|
}
|
|
478
|
+
|
|
479
|
+
// Override instance options per request
|
|
480
|
+
const text = await api.get("/readme.txt", {
|
|
481
|
+
timeout: 5
|
|
482
|
+
}).text();
|
|
332
483
|
```
|
|
333
484
|
|
|
334
485
|
### Advanced Usage
|
|
335
486
|
|
|
487
|
+
#### Jitter for Load Distribution
|
|
336
488
|
```ts
|
|
337
|
-
//
|
|
489
|
+
// Add randomized delay to prevent thundering herd
|
|
338
490
|
const response = await fetchy("https://api.example.com/data", {
|
|
339
|
-
|
|
491
|
+
jitter: 2, // Random delay up to 2 seconds before each request
|
|
340
492
|
retry: { maxAttempts: 3 }
|
|
341
493
|
});
|
|
494
|
+
```
|
|
342
495
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
496
|
+
#### Abort Signals
|
|
497
|
+
```ts
|
|
498
|
+
// Manual abort control
|
|
499
|
+
const controller = new AbortController();
|
|
500
|
+
const promise = fetchy("https://api.example.com/data", {
|
|
501
|
+
signal: controller.signal
|
|
348
502
|
});
|
|
349
503
|
|
|
350
|
-
setTimeout(() =>
|
|
504
|
+
setTimeout(() => controller.abort(), 5000);
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
await promise;
|
|
508
|
+
} catch (error) {
|
|
509
|
+
// Aborted after 5 seconds
|
|
510
|
+
}
|
|
351
511
|
|
|
352
|
-
|
|
353
|
-
|
|
512
|
+
// Combining timeout with manual abort
|
|
513
|
+
const controller = new AbortController();
|
|
514
|
+
await fetchy("https://api.example.com/data", {
|
|
515
|
+
timeout: 10,
|
|
516
|
+
signal: controller.signal
|
|
354
517
|
});
|
|
518
|
+
// Request will abort after 10 seconds OR when controller.abort() is called
|
|
519
|
+
```
|
|
355
520
|
|
|
521
|
+
#### Form Data and File Uploads
|
|
522
|
+
```ts
|
|
356
523
|
// Form data upload
|
|
357
524
|
const formData = new FormData();
|
|
358
|
-
formData.append("file", blob);
|
|
525
|
+
formData.append("file", blob, "filename.png");
|
|
359
526
|
formData.append("name", "example");
|
|
360
527
|
|
|
361
|
-
|
|
528
|
+
await fetchy("https://api.example.com/upload", {
|
|
529
|
+
method: "POST",
|
|
362
530
|
body: formData
|
|
363
531
|
});
|
|
364
532
|
|
|
365
533
|
// URL-encoded form
|
|
366
|
-
const params = new URLSearchParams();
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const response = await fetchy("https://api.example.com/form", {
|
|
534
|
+
const params = new URLSearchParams({ key: "value", foo: "bar" });
|
|
535
|
+
await fetchy("https://api.example.com/form", {
|
|
536
|
+
method: "POST",
|
|
370
537
|
body: params
|
|
371
538
|
});
|
|
372
539
|
```
|
|
373
540
|
|
|
374
|
-
|
|
541
|
+
#### Streaming with ReadableStream
|
|
542
|
+
```ts
|
|
543
|
+
// ReadableStream can be used via Request object
|
|
544
|
+
const stream = new ReadableStream({
|
|
545
|
+
start(controller) {
|
|
546
|
+
controller.enqueue(new TextEncoder().encode("chunk 1\n"));
|
|
547
|
+
controller.enqueue(new TextEncoder().encode("chunk 2\n"));
|
|
548
|
+
controller.close();
|
|
549
|
+
}
|
|
550
|
+
});
|
|
375
551
|
|
|
552
|
+
const response = await fetchy("https://api.example.com/stream", {
|
|
553
|
+
method: "POST",
|
|
554
|
+
body: stream
|
|
555
|
+
});
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
#### Retry-After Header Respect
|
|
559
|
+
```ts
|
|
560
|
+
// Automatically respects Retry-After, RateLimit-Reset, X-RateLimit-Reset headers
|
|
561
|
+
const data = await fetchy("https://api.example.com/rate-limited", {
|
|
562
|
+
retry: {
|
|
563
|
+
maxAttempts: 5,
|
|
564
|
+
interval: 1, // Minimum interval if header not present
|
|
565
|
+
respectHeaders: ["retry-after", "ratelimit-reset"]
|
|
566
|
+
}
|
|
567
|
+
}).json();
|
|
568
|
+
// If response has "Retry-After: 10", will wait 10 seconds before retry
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Type-Safe API Responses
|
|
376
572
|
```ts
|
|
377
573
|
interface ApiResponse<T> {
|
|
378
574
|
success: boolean;
|
|
@@ -386,46 +582,88 @@ interface Todo {
|
|
|
386
582
|
completed: boolean;
|
|
387
583
|
}
|
|
388
584
|
|
|
389
|
-
const response = await
|
|
390
|
-
|
|
391
|
-
"json"
|
|
392
|
-
);
|
|
585
|
+
const response = await fetchy("https://api.example.com/todos/1")
|
|
586
|
+
.json<ApiResponse<Todo>>();
|
|
393
587
|
|
|
394
588
|
if (response.success) {
|
|
395
589
|
console.log(response.data.title); // Fully typed
|
|
396
590
|
}
|
|
397
|
-
```
|
|
398
591
|
|
|
399
|
-
|
|
592
|
+
// With safe parsing
|
|
593
|
+
const result = await sfetchy("https://api.example.com/todos/1")
|
|
594
|
+
.json<ApiResponse<Todo>>();
|
|
400
595
|
|
|
401
|
-
|
|
596
|
+
if (result !== null && result.success) {
|
|
597
|
+
console.log(result.data.completed);
|
|
598
|
+
}
|
|
599
|
+
```
|
|
402
600
|
|
|
403
|
-
|
|
601
|
+
## Best Practices
|
|
404
602
|
|
|
603
|
+
### 1. Use Base URL for API Clients
|
|
405
604
|
```ts
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
605
|
+
const api = fy({
|
|
606
|
+
base: "https://api.example.com",
|
|
607
|
+
bearer: process.env.API_TOKEN,
|
|
608
|
+
timeout: 10
|
|
609
|
+
});
|
|
410
610
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
611
|
+
// All requests are relative to base
|
|
612
|
+
await api.get("/users").json();
|
|
613
|
+
await api.post("/posts", { body: data }).json();
|
|
414
614
|
```
|
|
415
615
|
|
|
416
|
-
###
|
|
616
|
+
### 2. Choose Safe vs. Throwing Behavior
|
|
617
|
+
```ts
|
|
618
|
+
// For critical operations: use regular methods (throws on error)
|
|
619
|
+
try {
|
|
620
|
+
const result = await fetchy(url).json();
|
|
621
|
+
// Process result
|
|
622
|
+
} catch (error) {
|
|
623
|
+
// Handle error explicitly
|
|
624
|
+
}
|
|
417
625
|
|
|
418
|
-
|
|
626
|
+
// For optional data: use safe methods (returns null)
|
|
627
|
+
const data = await fetchy(url).sjson();
|
|
628
|
+
if (data !== null) {
|
|
629
|
+
// Process data
|
|
630
|
+
}
|
|
631
|
+
// Continue regardless of result
|
|
632
|
+
```
|
|
419
633
|
|
|
420
|
-
|
|
634
|
+
### 3. Configure Retry for Resilience
|
|
635
|
+
```ts
|
|
636
|
+
// Aggressive retry for critical operations
|
|
637
|
+
const result = await fetchy(url, {
|
|
638
|
+
retry: {
|
|
639
|
+
maxAttempts: 5,
|
|
640
|
+
interval: 2,
|
|
641
|
+
maxInterval: 30,
|
|
642
|
+
retryOnTimeout: true
|
|
643
|
+
}
|
|
644
|
+
}).json();
|
|
421
645
|
|
|
422
|
-
|
|
646
|
+
// No retry for operations that must be fast
|
|
647
|
+
const data = await fetchy(url, {
|
|
648
|
+
retry: false,
|
|
649
|
+
timeout: 2
|
|
650
|
+
}).json();
|
|
651
|
+
```
|
|
423
652
|
|
|
424
|
-
|
|
653
|
+
### 4. Use Method Shortcuts for Clarity
|
|
654
|
+
```ts
|
|
655
|
+
const api = fy({ base: "https://api.example.com" });
|
|
425
656
|
|
|
426
|
-
|
|
657
|
+
// Clear and concise
|
|
658
|
+
await api.get("/users").json();
|
|
659
|
+
await api.post("/users", { body: newUser }).json();
|
|
660
|
+
await api.delete("/users/123");
|
|
427
661
|
|
|
428
|
-
|
|
662
|
+
// Instead of
|
|
663
|
+
await api.fetch("/users", { method: "GET" }).json();
|
|
664
|
+
await api.fetch("/users", { method: "POST", body: newUser }).json();
|
|
665
|
+
await api.fetch("/users/123", { method: "DELETE" });
|
|
666
|
+
```
|
|
429
667
|
|
|
430
668
|
## License
|
|
431
669
|
|