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