rust-fetch 1.1.0 โ 2.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/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +305 -663
- package/node.js +265 -0
- package/package.json +29 -5
- package/rust_fetch.d.ts +346 -93
- package/rust_fetch.js +1157 -792
- package/rust_fetch_bg.wasm +0 -0
- package/rust_fetch_bg.wasm.d.ts +54 -0
package/README.md
CHANGED
|
@@ -1,806 +1,448 @@
|
|
|
1
1
|
# rust-fetch
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/rust-fetch)
|
|
4
|
-
[](https://github.com/asemqaffaf/rust-fetch/actions/workflows/rust.yml)
|
|
5
|
+
[](https://github.com/asemqaffaf/rust-fetch/actions/workflows/rust.yml)
|
|
6
|
+
[](https://codecov.io/gh/asemqaffaf/rust-fetch)
|
|
7
|
+
[](#license)
|
|
5
8
|
[](https://webassembly.org/)
|
|
6
|
-
[](https://github.com/asemqaffaf/rust-fetch/releases)
|
|
7
|
-
|
|
8
|
-
### A high-performance, world-class HTTP Client Powered by WebAssembly
|
|
9
|
-
A high-performance HTTP client for JavaScript/TypeScript powered by WebAssembly. Built with Rust for blazing-fast, memory-safe HTTP operations in both browser and Node.js environments.
|
|
10
|
-
|
|
11
|
-
## Table of Contents
|
|
12
|
-
- [๐ What's New](#-whats-new-in-v110)
|
|
13
|
-
- [โจ Features](#-features)
|
|
14
|
-
- [๐ฆ Installation](#-installation)
|
|
15
|
-
- [๐ Quick Start](#-quick-start)
|
|
16
|
-
- [๐ API Reference](#-api-reference)
|
|
17
|
-
- [๐ง Advanced Usage](#-advanced-usage)
|
|
18
|
-
- [โก Performance](#-performance)
|
|
19
|
-
- [๐ Troubleshooting](#-troubleshooting)
|
|
20
|
-
- [๐ค Contributing](#-contributing)
|
|
21
|
-
- [๐ License](#-license)
|
|
22
|
-
|
|
23
|
-
## ๐ What's New in v1.1.0
|
|
24
|
-
|
|
25
|
-
- **Enhanced Error Context**: Improved error handling with detailed error types and proper error chaining
|
|
26
|
-
- **Advanced Request Options**: New options for timeout configuration and automatic retries
|
|
27
|
-
- **Comprehensive HTTP Methods**: Full support for all HTTP methods with a modern, unified API
|
|
28
|
-
- **Type-Safe Headers**: Better type safety for header manipulation
|
|
29
|
-
- **Performance Improvements**: Optimized WASM operations for faster HTTP requests
|
|
30
|
-
- **Backward Compatibility**: Methods like `post_json` and `post_text` are maintained for compatibility with v0.7.x
|
|
31
|
-
|
|
32
|
-
> **Note**: While legacy methods (`post_json`, `post_text`, etc.) are still supported, we recommend using the new unified API for new projects. The legacy methods will be maintained but won't receive new features.
|
|
33
|
-
|
|
34
|
-
## โจ Features
|
|
35
|
-
|
|
36
|
-
### Core Features
|
|
37
|
-
- ๐ **Type-Safe**: Full TypeScript support with compile-time type checking
|
|
38
|
-
- โก **High Performance**: Rust-powered core with WebAssembly optimization
|
|
39
|
-
- ๐ **Automatic Retries**: Built-in retry logic with exponential backoff
|
|
40
|
-
- ๐ก๏ธ **Robust Error Handling**: Detailed error types with proper context
|
|
41
|
-
|
|
42
|
-
### Developer Experience
|
|
43
|
-
- ๐ฏ **Simple API**: Intuitive interface for common use cases
|
|
44
|
-
- ๐ง **Flexible Configuration**: Advanced options for power users
|
|
45
|
-
- ๐ฆ **Zero Dependencies**: Minimal footprint in your application
|
|
46
|
-
- ๐ **Platform Agnostic**: Works in both browser and Node.js
|
|
47
|
-
|
|
48
|
-
### Technical Capabilities
|
|
49
|
-
- ๐ **Multiple Formats**: Support for JSON, text, and binary responses
|
|
50
|
-
- ๐ **Full HTTP Support**: All standard HTTP methods (GET, POST, PUT, etc.)
|
|
51
|
-
- ๐ **Header Management**: Type-safe header manipulation
|
|
52
|
-
- โฑ๏ธ **Timeout Control**: Configurable request timeouts
|
|
53
|
-
- ๐ **Interceptors**: Request/response modification pipeline
|
|
54
|
-
- ๐ **URL Handling**: Smart base URL and path combining
|
|
55
|
-
|
|
56
|
-
## ๐ฆ Installation
|
|
57
|
-
|
|
58
|
-
Choose your preferred package manager:
|
|
59
9
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
npm install rust-fetch
|
|
63
|
-
|
|
64
|
-
# Using yarn
|
|
65
|
-
yarn add rust-fetch
|
|
66
|
-
|
|
67
|
-
# Using pnpm
|
|
68
|
-
pnpm add rust-fetch
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Requirements
|
|
72
|
-
- Node.js 14+ or modern browsers
|
|
73
|
-
- WebAssembly support
|
|
74
|
-
|
|
75
|
-
## ๏ฟฝ Quick Start
|
|
76
|
-
|
|
77
|
-
### Basic Usage
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
import init, { WasmClient } from 'rust-fetch';
|
|
10
|
+
**A reliable HTTP client for JavaScript and TypeScript, written in Rust and compiled to WebAssembly.**
|
|
11
|
+
It runs in browsers, Web Workers and Node.js, and adds what `fetch()` leaves to you: timeouts, retries with backoff, typed errors, interceptors, multipart uploads and download progress.
|
|
81
12
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Create a client instance
|
|
86
|
-
const client = new WasmClient();
|
|
13
|
+
```js
|
|
14
|
+
import { WasmClient } from 'rust-fetch';
|
|
87
15
|
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
console.log(data);
|
|
16
|
+
const api = new WasmClient({ baseUrl: 'https://api.example.com/v1', timeoutMs: 10_000, retries: 3 });
|
|
17
|
+
const { body: user } = await api.get('/users/42');
|
|
91
18
|
```
|
|
92
19
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
#### Making GET Requests
|
|
96
|
-
```typescript
|
|
97
|
-
// Simple GET
|
|
98
|
-
const users = await client.get('/users');
|
|
20
|
+
## Contents
|
|
99
21
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
22
|
+
- [Features](#features)
|
|
23
|
+
- [Installation](#installation)
|
|
24
|
+
- [Quick start](#quick-start)
|
|
25
|
+
- [Guide](#guide): [client options](#client-options) ยท [requests](#making-requests) ยท [bodies](#request-bodies) ยท [responses](#responses) ยท [errors](#errors) ยท [timeouts](#timeouts) ยท [retries](#retries) ยท [cancellation](#cancellation) ยท [streaming](#streaming) ยท [progress](#download-progress) ยท [uploads](#file-uploads) ยท [interceptors](#interceptors) ยท [TypeScript](#typescript)
|
|
26
|
+
- [Legacy functions](#legacy-functions)
|
|
27
|
+
- [Using it from Rust](#using-it-from-rust)
|
|
28
|
+
- [Performance](#performance)
|
|
29
|
+
- [Roadmap](#roadmap)
|
|
30
|
+
- [Upgrading to 2.0](#upgrading-to-20)
|
|
31
|
+
- [Development](#development)
|
|
32
|
+
- [License](#license)
|
|
110
33
|
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
// Modern way to POST JSON data
|
|
114
|
-
const newUser = await client.post('/users', {
|
|
115
|
-
body: { name: 'John Doe', email: 'john@example.com' },
|
|
116
|
-
type: 'json'
|
|
117
|
-
});
|
|
34
|
+
## Features
|
|
118
35
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
36
|
+
- **Works everywhere**: browsers (directly or through bundlers like Vite and webpack), Web Workers and Node.js 20+. In Node.js there's no setup: `import` and go.
|
|
37
|
+
- **Timeouts** per attempt, covering the response body, not just the headers. The default is 30 s.
|
|
38
|
+
- **Retries** with exponential backoff and jitter. `Retry-After` is honoured, and POST/PATCH aren't retried unless you opt in.
|
|
39
|
+
- **Typed errors**: every rejection is a real `Error` with a `kind` (`HttpError`, `TimeoutError`, โฆ) and details like `status` and `body`.
|
|
40
|
+
- **Cancellation** with `AbortSignal`, including during retry backoff and while streaming.
|
|
41
|
+
- **Request bodies** from strings, bytes, `Blob`/`File`, `FormData` (multipart with files), `URLSearchParams`, or JSON.
|
|
42
|
+
- **Response decoding** by `Content-Type` (including `application/*+json`), or forced to `json`, `text`, `binary` or `stream`.
|
|
43
|
+
- **Streaming** responses as a `ReadableStream`, and **download progress** callbacks.
|
|
44
|
+
- **Interceptors** for requests and responses, e.g. auth headers and token refresh.
|
|
45
|
+
- **TypeScript** definitions for the whole API.
|
|
46
|
+
- **Tested**: 100% line coverage of the Rust core and of the Node.js transport, plus wasm tests and end-to-end tests of the packed npm package. See [Development](#development).
|
|
125
47
|
|
|
126
|
-
|
|
127
|
-
const legacyResponse = await client.post_json('/users', {
|
|
128
|
-
name: 'John Doe',
|
|
129
|
-
email: 'john@example.com'
|
|
130
|
-
});
|
|
131
|
-
```
|
|
48
|
+
## Installation
|
|
132
49
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
const data = await client.get('/users');
|
|
137
|
-
console.log('Success:', data);
|
|
138
|
-
} catch (error) {
|
|
139
|
-
if (error.status === 404) {
|
|
140
|
-
console.log('Resource not found');
|
|
141
|
-
} else if (error.name === 'NetworkError') {
|
|
142
|
-
console.log('Network issue:', error.message);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
50
|
+
```bash
|
|
51
|
+
npm install rust-fetch
|
|
145
52
|
```
|
|
146
53
|
|
|
147
|
-
##
|
|
54
|
+
## Quick start
|
|
148
55
|
|
|
149
|
-
|
|
56
|
+
**Node.js** (20+): the WebAssembly module loads on import.
|
|
150
57
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
// Legacy API
|
|
155
|
-
import { fetch_json, fetch_text, fetch_with_options } from 'rust-fetch';
|
|
156
|
-
|
|
157
|
-
// Modern API
|
|
58
|
+
```js
|
|
158
59
|
import { WasmClient } from 'rust-fetch';
|
|
159
|
-
const client = new WasmClient();
|
|
160
|
-
|
|
161
|
-
// GET requests
|
|
162
|
-
const data = await fetch_json('/api/data'); // Legacy
|
|
163
|
-
const data = await client.get('/api/data', { type: 'json' }); // Modern
|
|
164
|
-
|
|
165
|
-
// POST requests
|
|
166
|
-
const result = await fetch_with_options( // Legacy
|
|
167
|
-
'/api/users',
|
|
168
|
-
'POST',
|
|
169
|
-
{ 'Content-Type': 'application/json' },
|
|
170
|
-
JSON.stringify({ name: 'John' })
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
const result = await client.post('/api/users', { // Modern
|
|
174
|
-
body: { name: 'John' },
|
|
175
|
-
type: 'json'
|
|
176
|
-
});
|
|
177
60
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.error('Error:', error.message);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
try { // Modern
|
|
186
|
-
const data = await client.get('/api/data');
|
|
187
|
-
} catch (error) {
|
|
188
|
-
if (error instanceof NetworkError) {
|
|
189
|
-
console.error('Network issue:', error.message);
|
|
190
|
-
} else if (error instanceof HttpError) {
|
|
191
|
-
console.error('HTTP Error:', error.status);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
61
|
+
const client = new WasmClient();
|
|
62
|
+
const res = await client.get('https://jsonplaceholder.typicode.com/todos/1');
|
|
63
|
+
console.log(res.status, res.body.title);
|
|
194
64
|
```
|
|
195
65
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
1. **Better Type Safety**: The modern API provides full TypeScript support
|
|
199
|
-
2. **Enhanced Error Handling**: Detailed error types with proper context
|
|
200
|
-
3. **Advanced Features**: Access to interceptors, retry logic, and streaming
|
|
201
|
-
4. **Consistent Interface**: Unified API for all HTTP operations
|
|
202
|
-
5. **Future Updates**: New features will only be added to the modern API
|
|
203
|
-
|
|
204
|
-
## ๏ฟฝ๐ง Advanced Usage
|
|
66
|
+
**Browsers and bundlers**: call `init()` once before the first request. It downloads and compiles the `.wasm` file.
|
|
205
67
|
|
|
206
|
-
```
|
|
68
|
+
```js
|
|
207
69
|
import init, { WasmClient } from 'rust-fetch';
|
|
208
70
|
|
|
209
71
|
await init();
|
|
210
|
-
|
|
211
72
|
const client = new WasmClient();
|
|
212
|
-
|
|
213
|
-
const response = await client.get('https://jsonplaceholder.typicode.com/posts/1');
|
|
214
|
-
console.log(response.status); // 200
|
|
215
|
-
console.log(response.body); // { userId: 1, id: 1, title: '...', body: '...' }
|
|
216
73
|
```
|
|
217
74
|
|
|
218
|
-
|
|
219
|
-
Here's how to make a POST request with a JSON body:
|
|
220
|
-
|
|
221
|
-
```javascript
|
|
222
|
-
const data = { title: 'foo', body: 'bar', userId: 1 };
|
|
223
|
-
|
|
224
|
-
const response = await client.post_json('https://jsonplaceholder.typicode.com/posts', data);
|
|
225
|
-
console.log(response.status); // 201
|
|
226
|
-
console.log(response.body); // { id: 101, title: 'foo', body: 'bar', userId: 1 }
|
|
227
|
-
```
|
|
75
|
+
`init()` is safe to call more than once, and calling it in Node.js is harmless, so shared code can always call it.
|
|
228
76
|
|
|
229
|
-
|
|
230
|
-
Update a resource with PUT:
|
|
77
|
+
## Guide
|
|
231
78
|
|
|
232
|
-
|
|
233
|
-
const updateData = { id: 1, title: 'updated', body: 'updated content', userId: 1 };
|
|
234
|
-
|
|
235
|
-
const response = await client.put_json('https://jsonplaceholder.typicode.com/posts/1', updateData);
|
|
236
|
-
console.log(response.status); // 200
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
#### DELETE Request
|
|
240
|
-
Delete a resource:
|
|
241
|
-
|
|
242
|
-
```javascript
|
|
243
|
-
const response = await client.delete('https://jsonplaceholder.typicode.com/posts/1');
|
|
244
|
-
console.log(response.status); // 200
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
#### PATCH Request
|
|
248
|
-
Partially update a resource:
|
|
249
|
-
|
|
250
|
-
```javascript
|
|
251
|
-
const patchData = { title: 'patched title' };
|
|
252
|
-
|
|
253
|
-
const response = await client.patch_json('https://jsonplaceholder.typicode.com/posts/1', patchData);
|
|
254
|
-
console.log(response.status); // 200
|
|
255
|
-
```
|
|
79
|
+
### Client options
|
|
256
80
|
|
|
257
|
-
|
|
258
|
-
Make a custom request with full control over method, headers, and body:
|
|
259
|
-
|
|
260
|
-
```javascript
|
|
261
|
-
const headers = {
|
|
262
|
-
'Authorization': 'Bearer your-token',
|
|
263
|
-
'X-Custom-Header': 'custom-value'
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
const body = { data: 'custom request body' };
|
|
267
|
-
|
|
268
|
-
const response = await client.request('POST', 'https://api.example.com/data', headers, body);
|
|
269
|
-
console.log(response.status);
|
|
270
|
-
console.log(response.headers);
|
|
271
|
-
console.log(response.body);
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
## ๐ง Advanced Usage
|
|
275
|
-
|
|
276
|
-
### Client Configuration
|
|
277
|
-
|
|
278
|
-
```typescript
|
|
81
|
+
```ts
|
|
279
82
|
const client = new WasmClient({
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
//
|
|
284
|
-
headers: {
|
|
285
|
-
'Authorization': 'Bearer token',
|
|
286
|
-
'X-Api-Version': '2.0'
|
|
287
|
-
},
|
|
288
|
-
|
|
289
|
-
// Timeout settings
|
|
290
|
-
timeout: 5000, // 5 seconds
|
|
291
|
-
|
|
292
|
-
// Retry configuration
|
|
293
|
-
retry: {
|
|
294
|
-
attempts: 3,
|
|
295
|
-
backoff: 'exponential',
|
|
296
|
-
conditions: ['network', '5xx']
|
|
297
|
-
},
|
|
298
|
-
|
|
299
|
-
// Response type preferences
|
|
300
|
-
responseType: 'json',
|
|
301
|
-
|
|
302
|
-
// Custom error handling
|
|
303
|
-
errorHandler: (error) => {
|
|
304
|
-
// Custom error logging or transformation
|
|
305
|
-
console.error(`[${error.code}]`, error.message);
|
|
306
|
-
throw error;
|
|
307
|
-
}
|
|
83
|
+
baseUrl: 'https://api.example.com/v1', // relative request URLs are appended to this
|
|
84
|
+
headers: { Authorization: 'Bearer โฆ' }, // sent with every request
|
|
85
|
+
timeoutMs: 10_000, // per attempt; 0 disables. Default: 30000
|
|
86
|
+
retries: 3, // or a RetryOptions object, see Retries
|
|
308
87
|
});
|
|
309
88
|
```
|
|
310
89
|
|
|
311
|
-
|
|
90
|
+
Invalid options throw an `InvalidInputError` right away, from the constructor.
|
|
312
91
|
|
|
313
|
-
|
|
314
|
-
// Add request interceptors
|
|
315
|
-
client.addRequestInterceptor((config) => {
|
|
316
|
-
// Modify request config
|
|
317
|
-
config.headers['X-Timestamp'] = Date.now();
|
|
318
|
-
return config;
|
|
319
|
-
});
|
|
92
|
+
### Making requests
|
|
320
93
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
);
|
|
94
|
+
| Method | Use |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `get(url, options?)`, `head(url, options?)`, `delete(url, options?)` | Requests without a body |
|
|
97
|
+
| `post(url, init?)`, `put(url, init?)`, `patch(url, init?)` | `init.body` is the body; any other [request option](#request-options) can go next to it |
|
|
98
|
+
| `fetch(url, init?)` | `fetch`-style, with `init.method` (default `GET`) |
|
|
99
|
+
| `post_json`, `put_json`, `patch_json(url, body, options?)` | Always send `body` as JSON |
|
|
100
|
+
| `request(method, url, headers?, body?, options?)` | Fully positional |
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
await client.post('/users', { body: { name: 'Zoรซ' } });
|
|
104
|
+
await client.get('/search', { params: { q: 'rust wasm', page: 2, tag: ['a', 'b'] } }); // ?q=rust+wasm&page=2&tag=a&tag=b
|
|
105
|
+
await client.fetch('/users/1', { method: 'DELETE', headers: { 'X-Reason': 'cleanup' } });
|
|
335
106
|
```
|
|
336
107
|
|
|
337
|
-
|
|
108
|
+
Relative URLs need a `baseUrl`. In a browser page without one, they resolve against the page URL. Absolute URLs always win over `baseUrl`. `TRACE` and `CONNECT` are rejected, because browsers' `fetch()` forbids them.
|
|
338
109
|
|
|
339
|
-
|
|
340
|
-
// Concurrent requests
|
|
341
|
-
const [users, posts] = await Promise.all([
|
|
342
|
-
client.get('/users'),
|
|
343
|
-
client.get('/posts')
|
|
344
|
-
]);
|
|
110
|
+
#### Request options
|
|
345
111
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
112
|
+
| Option | Type | Description |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| `headers` | object, `Headers`, `Map` or `[name, value][]` | Override client headers with the same name. Numbers and booleans are converted to strings. |
|
|
115
|
+
| `params` | object or `URLSearchParams` | Appended to the query string. Arrays repeat the key; `null`/`undefined` are skipped. |
|
|
116
|
+
| `timeoutMs` | number | Overrides the client timeout; `0` disables it |
|
|
117
|
+
| `responseType` | `'auto'` \| `'json'` \| `'text'` \| `'binary'` \| `'stream'` | See [Responses](#responses) |
|
|
118
|
+
| `signal` | `AbortSignal` | See [Cancellation](#cancellation) |
|
|
119
|
+
| `onDownloadProgress` | `({ loaded, total }) => void` | See [Download progress](#download-progress) |
|
|
351
120
|
|
|
352
|
-
|
|
353
|
-
setTimeout(() => controller.abort(), 5000);
|
|
121
|
+
### Request bodies
|
|
354
122
|
|
|
355
|
-
|
|
356
|
-
const stream = await client.get('/large-file', {
|
|
357
|
-
responseType: 'stream'
|
|
358
|
-
});
|
|
359
|
-
for await (const chunk of stream) {
|
|
360
|
-
// Process chunks
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// File upload with progress
|
|
364
|
-
const response = await client.post('/upload', formData, {
|
|
365
|
-
onUploadProgress: (progress) => {
|
|
366
|
-
console.log(`${progress.loaded} / ${progress.total}`);
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
```
|
|
123
|
+
The body's type decides how it's sent. A `Content-Type` header you set yourself is always kept, except for `FormData`, which needs its own boundary.
|
|
370
124
|
|
|
371
|
-
|
|
125
|
+
| Body | Sent as | Default `Content-Type` |
|
|
126
|
+
| --- | --- | --- |
|
|
127
|
+
| `string` | as-is | `text/plain` |
|
|
128
|
+
| `Uint8Array`, `ArrayBuffer`, `DataView`, `Buffer` | bytes | `application/octet-stream` |
|
|
129
|
+
| `Blob` / `File` | bytes | the blob's `type` |
|
|
130
|
+
| `FormData` | `multipart/form-data`, including files | `multipart/form-data; boundary=โฆ` |
|
|
131
|
+
| `URLSearchParams` | form-encoded | `application/x-www-form-urlencoded;charset=UTF-8` |
|
|
132
|
+
| anything else (objects, arrays, numbers, booleans) | `JSON.stringify` | `application/json` |
|
|
372
133
|
|
|
373
|
-
|
|
374
|
-
import { fetch_with_options } from 'rust-fetch';
|
|
134
|
+
`null` and `undefined` mean no body. The `*_json` methods are the exception: they always send JSON, so `post_json(url, null)` sends `null`. `ReadableStream` request bodies aren't supported; send a `Blob` or bytes instead.
|
|
375
135
|
|
|
376
|
-
|
|
377
|
-
const method = 'POST';
|
|
378
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
379
|
-
const body = JSON.stringify({ userId: 1, title: 'foo', body: 'bar' });
|
|
136
|
+
### Responses
|
|
380
137
|
|
|
381
|
-
|
|
382
|
-
.then(response => console.log(response))
|
|
383
|
-
.catch(error => console.error('Error:', error));
|
|
138
|
+
Successful (2xx) requests resolve to:
|
|
384
139
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
140
|
+
```ts
|
|
141
|
+
interface HttpResponse<T = any> {
|
|
142
|
+
status: number; // 200
|
|
143
|
+
statusText: string; // "OK"
|
|
144
|
+
ok: boolean; // always true: non-2xx responses reject with an HttpError
|
|
145
|
+
url: string; // final URL after redirects
|
|
146
|
+
headers: Record<string, string>; // lower-cased names; repeated headers joined with ", "
|
|
147
|
+
body: T;
|
|
148
|
+
}
|
|
393
149
|
```
|
|
394
150
|
|
|
395
|
-
|
|
151
|
+
With the default `responseType: 'auto'`, `body` is:
|
|
152
|
+
- **parsed JSON** for `application/json` and `application/*+json` (e.g. `problem+json`), falling back to text if it isn't valid JSON
|
|
153
|
+
- **a string** for `text/*`, XML, JavaScript and form-encoded responses
|
|
154
|
+
- **a `Uint8Array`** for anything else
|
|
155
|
+
- **`null`** when the body is empty (e.g. `204` or `HEAD`)
|
|
396
156
|
|
|
397
|
-
|
|
157
|
+
Force a type with `responseType: 'json'` (fails with a `ParseError` if the body isn't JSON), `'text'`, `'binary'` or `'stream'`. JSON is parsed by the JS engine's `JSON.parse`, exactly like `Response.json()`.
|
|
398
158
|
|
|
399
|
-
|
|
400
|
-
- **`types`**: Core types like `Method`, `Headers`, `Response`, etc.
|
|
401
|
-
- **`client`**: The main HTTP client with builder pattern
|
|
402
|
-
- **`http`**: High-level convenience functions for common use cases
|
|
159
|
+
### Errors
|
|
403
160
|
|
|
404
|
-
|
|
161
|
+
Every rejection is an `Error` whose `name` and `kind` are one of:
|
|
405
162
|
|
|
406
|
-
|
|
163
|
+
| `kind` | When | Extra fields |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| `HttpError` | Non-2xx response | `status`, `statusText`, `body` (full response body as text) |
|
|
166
|
+
| `TimeoutError` | An attempt exceeded `timeoutMs` | `timeoutMs` |
|
|
167
|
+
| `NetworkError` | DNS, connection, TLS or CORS failure, or a body cut off mid-transfer | |
|
|
168
|
+
| `CancelledError` | The `signal` was aborted | |
|
|
169
|
+
| `InvalidInputError` | Bad URL, method, header, option or body. Rejected before anything is sent. | `parameter` |
|
|
170
|
+
| `ParseError` | `responseType: 'json'` and the body isn't JSON | |
|
|
171
|
+
| `JsInteropError` | Unexpected JS โ Wasm conversion failure | |
|
|
407
172
|
|
|
408
|
-
```
|
|
173
|
+
```js
|
|
409
174
|
try {
|
|
410
|
-
|
|
411
|
-
console.log('Success:', data);
|
|
175
|
+
await client.get('/users/9');
|
|
412
176
|
} catch (error) {
|
|
413
|
-
if (error
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
console.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
console.error('HTTP Error:', error.status, error.statusText);
|
|
420
|
-
console.log('Response body:', error.body);
|
|
421
|
-
} else if (error instanceof TimeoutError) {
|
|
422
|
-
console.error('Request timed out after:', error.durationMs, 'ms');
|
|
177
|
+
if (error.kind === 'HttpError' && error.status === 404) {
|
|
178
|
+
// not found
|
|
179
|
+
} else if (error.kind === 'TimeoutError') {
|
|
180
|
+
console.warn(`gave up after ${error.timeoutMs} ms`);
|
|
181
|
+
} else {
|
|
182
|
+
throw error;
|
|
423
183
|
}
|
|
424
184
|
}
|
|
425
185
|
```
|
|
426
186
|
|
|
427
|
-
|
|
187
|
+
Long error bodies (such as an HTML error page) are truncated to 200 characters in `message`. `error.body` always has the full text.
|
|
428
188
|
|
|
429
|
-
|
|
189
|
+
### Timeouts
|
|
430
190
|
|
|
431
|
-
|
|
432
|
-
import { WasmClient, RequestConfig, Response } from 'rust-fetch';
|
|
191
|
+
`timeoutMs` limits each attempt, from sending the request to receiving the last byte of the body. When retries are on, each attempt gets the full timeout. For `responseType: 'stream'`, the timeout covers only the wait for the headers; after that, reading the stream is up to you.
|
|
433
192
|
|
|
434
|
-
|
|
435
|
-
id: number;
|
|
436
|
-
name: string;
|
|
437
|
-
email: string;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const client = new WasmClient();
|
|
193
|
+
### Retries
|
|
441
194
|
|
|
442
|
-
|
|
443
|
-
const user: Response<User> = await client.get<User>('/users/1');
|
|
195
|
+
Retries are off by default. `retries: 3` enables them with these defaults; pass an object to change them:
|
|
444
196
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
197
|
+
```ts
|
|
198
|
+
new WasmClient({
|
|
199
|
+
retries: {
|
|
200
|
+
maxRetries: 3, // retries after the first attempt
|
|
201
|
+
initialDelayMs: 100, // first backoff
|
|
202
|
+
maxDelayMs: 10_000, // cap for any delay, including Retry-After
|
|
203
|
+
multiplier: 2, // exponential backoff: 100, 200, 400 โฆ ms, each randomized to 50โ100%
|
|
204
|
+
retryOn: [408, 429, 500, 502, 503, 504],
|
|
205
|
+
retryOnNetworkError: true,
|
|
206
|
+
retryOnTimeout: true,
|
|
207
|
+
retryNonIdempotent: false, // set to true to also retry POST and PATCH
|
|
208
|
+
},
|
|
449
209
|
});
|
|
450
|
-
|
|
451
|
-
// Custom request with type safety
|
|
452
|
-
const config: RequestConfig<User> = {
|
|
453
|
-
method: 'POST',
|
|
454
|
-
headers: { 'X-Custom-Header': 'value' },
|
|
455
|
-
body: { name: 'John', email: 'john@example.com' }
|
|
456
|
-
};
|
|
457
|
-
|
|
458
|
-
const response = await client.request<User>('/users', config);
|
|
459
210
|
```
|
|
460
211
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
212
|
+
- A `Retry-After` header in seconds is honoured, capped at `maxDelayMs`.
|
|
213
|
+
- POST and PATCH aren't retried by default. The server may already have acted on a failed attempt, so retrying can duplicate work.
|
|
214
|
+
- `InvalidInputError`, `ParseError` and unlisted statuses are never retried.
|
|
464
215
|
|
|
465
|
-
|
|
216
|
+
### Cancellation
|
|
466
217
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
await init(); // Initialize WASM module
|
|
473
|
-
const client = new WasmClient();
|
|
218
|
+
```js
|
|
219
|
+
const controller = new AbortController();
|
|
220
|
+
const promise = client.get('/report', { signal: controller.signal });
|
|
221
|
+
controller.abort(); // rejects with a CancelledError
|
|
474
222
|
|
|
475
|
-
//
|
|
476
|
-
const client = new WasmClient({
|
|
477
|
-
timeout: 5000, // 5 seconds timeout
|
|
478
|
-
retryAttempts: 3, // Retry failed requests 3 times
|
|
479
|
-
baseUrl: 'https://api.example.com'
|
|
480
|
-
});
|
|
223
|
+
await client.get('/report', { signal: AbortSignal.timeout(5_000) }); // total deadline, including retries
|
|
481
224
|
```
|
|
482
225
|
|
|
483
|
-
|
|
226
|
+
Aborting cancels the request in flight, any pending retry delay, and a body being streamed.
|
|
484
227
|
|
|
485
|
-
|
|
228
|
+
### Streaming
|
|
486
229
|
|
|
487
|
-
|
|
488
|
-
```javascript
|
|
489
|
-
const response = await client.get('https://api.example.com/users');
|
|
490
|
-
// With query parameters
|
|
491
|
-
const response = await client.get('https://api.example.com/users', {
|
|
492
|
-
params: { page: 1, limit: 10 }
|
|
493
|
-
});
|
|
494
|
-
```
|
|
230
|
+
`responseType: 'stream'` resolves as soon as the headers arrive. `body` is then a `ReadableStream<Uint8Array>`:
|
|
495
231
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
});
|
|
232
|
+
```js
|
|
233
|
+
const res = await client.get('/export.csv', { responseType: 'stream' });
|
|
234
|
+
for await (const chunk of res.body) {
|
|
235
|
+
process(chunk); // Uint8Array
|
|
236
|
+
}
|
|
502
237
|
```
|
|
503
238
|
|
|
504
|
-
|
|
505
|
-
```javascript
|
|
506
|
-
const response = await client.put_json('/users/1', {
|
|
507
|
-
name: 'John Updated',
|
|
508
|
-
email: 'john.updated@example.com'
|
|
509
|
-
});
|
|
510
|
-
```
|
|
239
|
+
`for await` over a stream works in Node.js, Chrome and Firefox. Safari doesn't support it yet, so use `res.body.getReader()` there. Retries apply until the headers arrive, and error statuses still reject with an `HttpError`.
|
|
511
240
|
|
|
512
|
-
|
|
513
|
-
```javascript
|
|
514
|
-
const response = await client.delete('/users/1');
|
|
515
|
-
// With body
|
|
516
|
-
const response = await client.delete('/users/1', {
|
|
517
|
-
body: { reason: 'Account closed' }
|
|
518
|
-
});
|
|
519
|
-
```
|
|
241
|
+
### Download progress
|
|
520
242
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
243
|
+
```js
|
|
244
|
+
await client.get('/video.mp4', {
|
|
245
|
+
responseType: 'binary',
|
|
246
|
+
onDownloadProgress: ({ loaded, total }) => {
|
|
247
|
+
console.log(total ? `${Math.round((loaded / total) * 100)}%` : `${loaded} bytes`);
|
|
248
|
+
},
|
|
525
249
|
});
|
|
526
250
|
```
|
|
527
251
|
|
|
528
|
-
|
|
529
|
-
```javascript
|
|
530
|
-
const response = await client.head('/users/1');
|
|
531
|
-
console.log(response.headers);
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
#### Advanced Usage
|
|
535
|
-
|
|
536
|
-
##### Custom Request
|
|
537
|
-
```javascript
|
|
538
|
-
const response = await client.request({
|
|
539
|
-
method: 'POST',
|
|
540
|
-
url: '/data',
|
|
541
|
-
headers: {
|
|
542
|
-
'Authorization': 'Bearer token',
|
|
543
|
-
'X-Custom-Header': 'value'
|
|
544
|
-
},
|
|
545
|
-
body: { data: 'example' },
|
|
546
|
-
timeout: 10000,
|
|
547
|
-
retry: {
|
|
548
|
-
attempts: 3,
|
|
549
|
-
backoff: 'exponential'
|
|
550
|
-
}
|
|
551
|
-
});
|
|
552
|
-
```
|
|
252
|
+
The callback runs after each chunk of a successful response, including streamed ones. `total` comes from `Content-Length` and is `null` when the server doesn't send it. Exceptions thrown by the callback are ignored.
|
|
553
253
|
|
|
554
|
-
|
|
254
|
+
Upload progress isn't available, because `fetch()` doesn't report it.
|
|
555
255
|
|
|
556
|
-
|
|
256
|
+
### File uploads
|
|
557
257
|
|
|
558
|
-
|
|
559
|
-
```javascript
|
|
560
|
-
// Legacy way
|
|
561
|
-
import { fetch_json } from 'rust-fetch';
|
|
562
|
-
const data = await fetch_json('https://api.example.com/data');
|
|
258
|
+
Pass a `FormData` and it's sent as `multipart/form-data`, files included:
|
|
563
259
|
|
|
564
|
-
|
|
565
|
-
const
|
|
566
|
-
|
|
260
|
+
```js
|
|
261
|
+
const form = new FormData();
|
|
262
|
+
form.append('title', 'Quarterly report');
|
|
263
|
+
form.append('file', fileInput.files[0]); // or new File([bytes], 'report.pdf', { type: 'application/pdf' })
|
|
264
|
+
await client.post('/upload', { body: form });
|
|
567
265
|
```
|
|
568
266
|
|
|
569
|
-
|
|
570
|
-
```javascript
|
|
571
|
-
// Legacy way
|
|
572
|
-
import { fetch_text } from 'rust-fetch';
|
|
573
|
-
const html = await fetch_text('https://example.com');
|
|
267
|
+
A single `Blob` or `File` body is sent as raw bytes with the blob's type.
|
|
574
268
|
|
|
575
|
-
|
|
576
|
-
const client = new WasmClient();
|
|
577
|
-
const html = await client.get('https://example.com', { type: 'text' });
|
|
578
|
-
```
|
|
269
|
+
### Interceptors
|
|
579
270
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
'POST',
|
|
587
|
-
{ 'Content-Type': 'application/json' },
|
|
588
|
-
JSON.stringify({ name: 'John Doe' })
|
|
589
|
-
);
|
|
590
|
-
```
|
|
271
|
+
```js
|
|
272
|
+
// Add a header to every request (may be async)
|
|
273
|
+
client.addRequestInterceptor((request) => {
|
|
274
|
+
request.headers.authorization = `Bearer ${getToken()}`;
|
|
275
|
+
// mutate in place, or return a new { method, url, headers, body }
|
|
276
|
+
});
|
|
591
277
|
|
|
592
|
-
|
|
593
|
-
|
|
278
|
+
// Resolve to the body instead of the whole response
|
|
279
|
+
client.addResponseInterceptor((response) => response.body);
|
|
594
280
|
|
|
595
|
-
|
|
596
|
-
|
|
281
|
+
// Report failures, then keep rejecting
|
|
282
|
+
client.addResponseInterceptor(undefined, (error) => {
|
|
283
|
+
reportError(error);
|
|
284
|
+
throw error;
|
|
285
|
+
});
|
|
597
286
|
```
|
|
598
287
|
|
|
599
|
-
|
|
600
|
-
> **Note**: This method is maintained for backward compatibility with v0.7.x. For new code, prefer using `client.post(url, { body, type: 'json' })`.
|
|
288
|
+
An `onRejected` handler can also recover by returning a value, for example the result of retrying the request after refreshing a token.
|
|
601
289
|
|
|
602
|
-
|
|
290
|
+
- Request interceptors run in order before each request. They see `{ method, url, headers, body }`, where `headers` holds the per-request headers. Client default headers are added afterwards, and a request header with the same name wins.
|
|
291
|
+
- Response interceptors chain like `promise.then(onFulfilled, onRejected)`. Their return values are what the call resolves to. Throw to reject.
|
|
292
|
+
- `addRequestInterceptor` and `addResponseInterceptor` return an id for `ejectInterceptor(id)`. `clearInterceptors()` removes all of them.
|
|
293
|
+
- A request uses the interceptors registered when it started.
|
|
603
294
|
|
|
604
|
-
|
|
605
|
-
// Legacy way (backward compatible)
|
|
606
|
-
const response = await client.post_json('https://api.example.com/users', {
|
|
607
|
-
name: 'Jane Doe',
|
|
608
|
-
email: 'jane@example.com'
|
|
609
|
-
});
|
|
295
|
+
### TypeScript
|
|
610
296
|
|
|
611
|
-
|
|
612
|
-
const response = await client.post('https://api.example.com/users', {
|
|
613
|
-
body: { name: 'Jane Doe', email: 'jane@example.com' },
|
|
614
|
-
type: 'json'
|
|
615
|
-
});
|
|
616
|
-
```
|
|
297
|
+
Type definitions ship with the package and need TypeScript 5.2 or newer. Besides `WasmClient`, it exports the option and result types `ClientOptions`, `RequestOptions`, `FetchInit`, `RetryOptions`, `HttpResponse`, `RustFetchError`, `DownloadProgress` and `InterceptedRequest`.
|
|
617
298
|
|
|
618
|
-
|
|
619
|
-
|
|
299
|
+
```ts
|
|
300
|
+
import { WasmClient, type HttpResponse, type RustFetchError } from 'rust-fetch';
|
|
620
301
|
|
|
621
|
-
|
|
622
|
-
const response = await client.put_json('https://api.example.com/users/1', {
|
|
623
|
-
name: 'Jane Doe Updated',
|
|
624
|
-
email: 'jane.updated@example.com'
|
|
625
|
-
});
|
|
626
|
-
```
|
|
302
|
+
interface User { id: number; name: string }
|
|
627
303
|
|
|
628
|
-
|
|
629
|
-
|
|
304
|
+
const res: HttpResponse<User> = await client.get('/users/42');
|
|
305
|
+
res.body.name; // string
|
|
630
306
|
|
|
631
|
-
|
|
632
|
-
|
|
307
|
+
try {
|
|
308
|
+
await client.get('/users/9');
|
|
309
|
+
} catch (e) {
|
|
310
|
+
const error = e as RustFetchError;
|
|
311
|
+
if (error.kind === 'HttpError') console.log(error.status);
|
|
312
|
+
}
|
|
633
313
|
```
|
|
634
314
|
|
|
635
|
-
|
|
636
|
-
Performs a PATCH request with JSON body.
|
|
315
|
+
## Legacy functions
|
|
637
316
|
|
|
638
|
-
|
|
639
|
-
const response = await client.patch_json('https://api.example.com/users/1', {
|
|
640
|
-
email: 'newemail@example.com'
|
|
641
|
-
});
|
|
642
|
-
```
|
|
317
|
+
These standalone functions still work. Each call uses a default client: 30 s timeout, no retries.
|
|
643
318
|
|
|
644
|
-
|
|
645
|
-
|
|
319
|
+
| Function | Resolves to |
|
|
320
|
+
| --- | --- |
|
|
321
|
+
| `fetch_json(url)` / `fetch_json_promise(url)` | Parsed JSON, whatever the `Content-Type`; `null` for an empty body |
|
|
322
|
+
| `fetch_text(url)` | The body as a string |
|
|
323
|
+
| `fetch_with_options(url, method, headers, body?)` | The full `HttpResponse` |
|
|
324
|
+
| `create_client()` | `new WasmClient()` |
|
|
325
|
+
| `fetch_wasm_json`, `fetch_wasm_map`, `fetch_wasm_api`, `fetch_wasm_html` | Deprecated aliases of `fetch_json` / `fetch_text` |
|
|
646
326
|
|
|
647
|
-
|
|
648
|
-
const response = await client.head('https://api.example.com/users/1');
|
|
649
|
-
console.log('Headers:', response.headers);
|
|
650
|
-
```
|
|
327
|
+
## Using it from Rust
|
|
651
328
|
|
|
652
|
-
|
|
653
|
-
Performs a custom HTTP request with full control.
|
|
329
|
+
The same client is a Rust library, for native code (on tokio) and for your own wasm crates:
|
|
654
330
|
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
'https://api.example.com/data',
|
|
659
|
-
{ 'Authorization': 'Bearer token', 'X-Custom-Header': 'value' },
|
|
660
|
-
{ data: 'example' }
|
|
661
|
-
);
|
|
662
|
-
```
|
|
331
|
+
```rust
|
|
332
|
+
use rust_fetch::{Client, Multipart, types::RetryConfig};
|
|
333
|
+
use std::time::Duration;
|
|
663
334
|
|
|
335
|
+
let client = Client::builder()
|
|
336
|
+
.base_url("https://api.example.com/v1")
|
|
337
|
+
.timeout(Duration::from_secs(10))
|
|
338
|
+
.retry_config(RetryConfig { max_retries: 3, ..Default::default() })
|
|
339
|
+
.build()?;
|
|
664
340
|
|
|
665
|
-
|
|
341
|
+
let user = client.get("users/42").await?;
|
|
342
|
+
println!("{}", user.json().unwrap()["name"]);
|
|
666
343
|
|
|
667
|
-
|
|
344
|
+
let pdf: Vec<u8> = std::fs::read("report.pdf")?;
|
|
345
|
+
let form = Multipart::new().text("title", "Report").file("file", "report.pdf", Some("application/pdf".into()), pdf);
|
|
346
|
+
client.post("upload").multipart(&form).send().await?;
|
|
668
347
|
|
|
669
|
-
|
|
670
|
-
Operation Native Fetch rust-fetch
|
|
671
|
-
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
672
|
-
Initial Load 0ms ~150ms
|
|
673
|
-
Simple GET 324ms 298ms
|
|
674
|
-
Large POST 567ms 489ms
|
|
675
|
-
Concurrent (10) 890ms 612ms
|
|
676
|
-
Memory Usage ~8MB ~4.2MB
|
|
348
|
+
let mut chunks = client.get_streaming("export.csv").await?.into_stream(); // a futures::Stream of Vec<u8>
|
|
677
349
|
```
|
|
678
350
|
|
|
679
|
-
|
|
351
|
+
Request futures are `Send`, so you can `tokio::spawn` them. Progress callbacks (`on_download_progress`) must be `Send + Sync` on native targets.
|
|
680
352
|
|
|
681
|
-
|
|
682
|
-
- **Concurrent Requests**: Use `Promise.all` for parallel requests
|
|
683
|
-
- **Memory Usage**: Enable streaming for large responses
|
|
684
|
-
- **Cold Starts**: Preload the WASM module during app initialization
|
|
353
|
+
## Performance
|
|
685
354
|
|
|
686
|
-
|
|
355
|
+
**In Node.js, rust-fetch is as fast as or faster than the built-in `fetch()` for typical API traffic.** Node's `fetch()` implements browser semantics (WHATWG streams) on top of its sockets. rust-fetch uses its own lean transport on `node:http`/`node:https` with keep-alive. Retries, timeouts, validation and errors still come from the Rust core.
|
|
687
356
|
|
|
688
|
-
|
|
689
|
-
|---------|----------------|
|
|
690
|
-
| Chrome | 57+ |
|
|
691
|
-
| Firefox | 52+ |
|
|
692
|
-
| Safari | 11+ |
|
|
693
|
-
| Edge | 16+ |
|
|
694
|
-
| Node.js | 14+ |
|
|
357
|
+
`npm run bench` (Node 22.13, Apple M1 Pro, local server, median of 5 alternating rounds):
|
|
695
358
|
|
|
696
|
-
|
|
359
|
+
| Scenario | native `fetch` | rust-fetch | rust-fetch / native |
|
|
360
|
+
| --- | ---: | ---: | ---: |
|
|
361
|
+
| Small JSON, sequential | 10,558 req/s | 10,634 req/s | **101%** |
|
|
362
|
+
| Small JSON, 50 concurrent | 13,401 req/s | 13,895 req/s | **104%** |
|
|
363
|
+
| JSON 0.6 MB | 219 req/s | 203 req/s | 92% |
|
|
364
|
+
| Binary 8 MB | 177 req/s | 181 req/s | **102%** |
|
|
697
365
|
|
|
698
|
-
|
|
366
|
+
Large JSON is dominated by `JSON.parse`, which both clients pay; the remaining gap is extra garbage collection from crossing into Wasm.
|
|
699
367
|
|
|
700
|
-
|
|
701
|
-
- โ
Ensure `application/wasm` MIME type is supported
|
|
702
|
-
- โ
Verify WASM file is being served correctly
|
|
703
|
-
- โ
Check `await init()` is called before making requests
|
|
368
|
+
**In browsers**, `fetch()` is the only way to make requests, so rust-fetch uses it and can't be faster than it. The difference is a fraction of a millisecond per request, which matters little once a request crosses a real network.
|
|
704
369
|
|
|
705
|
-
|
|
706
|
-
// Correct initialization
|
|
707
|
-
await init();
|
|
708
|
-
const client = new WasmClient();
|
|
370
|
+
## Roadmap
|
|
709
371
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
372
|
+
- [x] POST, PUT, PATCH, DELETE, HEAD, OPTIONS
|
|
373
|
+
- [x] Request header customization
|
|
374
|
+
- [x] Comprehensive test suite: Rust unit and integration tests (100% line coverage, enforced in CI), wasm tests, and JS end-to-end tests
|
|
375
|
+
- [x] Streaming responses (`responseType: 'stream'`; `send_streaming()` in Rust)
|
|
376
|
+
- [x] Request cancellation (`AbortSignal`)
|
|
377
|
+
- [x] Performance benchmarks against native fetch (`npm run bench`)
|
|
378
|
+
- [x] Multipart form data and file uploads
|
|
379
|
+
- [x] Request/response interceptors
|
|
380
|
+
- [x] Download progress tracking
|
|
381
|
+
- [ ] Upload progress tracking: not possible on top of `fetch()`, which has no upload progress events
|
|
382
|
+
- [ ] WebSocket support
|
|
383
|
+
- Handled by the runtime, not by rust-fetch:
|
|
384
|
+
- **Connection pooling:** browsers pool connections for `fetch()`; in Node.js rust-fetch reuses connections through Node's default keep-alive agents.
|
|
385
|
+
- **Proxies:** in Node.js, install a proxy agent as `http.globalAgent` / `https.globalAgent` (for example from `https-proxy-agent`). In browsers, use the system settings.
|
|
386
|
+
- **HTTP/2:** browsers negotiate it automatically; the Node.js transport uses HTTP/1.1 with keep-alive.
|
|
714
387
|
|
|
715
|
-
|
|
716
|
-
- โ
Verify target server allows CORS
|
|
717
|
-
- โ
Use HTTPS URLs in production
|
|
718
|
-
- โ
Consider using a proxy for non-CORS APIs
|
|
388
|
+
## Upgrading to 2.0
|
|
719
389
|
|
|
720
|
-
|
|
721
|
-
// Using a proxy
|
|
722
|
-
const client = new WasmClient({
|
|
723
|
-
baseUrl: '/api', // Local proxy endpoint
|
|
724
|
-
proxy: {
|
|
725
|
-
target: 'https://api.example.com',
|
|
726
|
-
changeOrigin: true
|
|
727
|
-
}
|
|
728
|
-
});
|
|
729
|
-
```
|
|
390
|
+
2.0 fixes long-standing bugs, and some code may have depended on the old behaviour. Check these points:
|
|
730
391
|
|
|
731
|
-
|
|
732
|
-
-
|
|
733
|
-
-
|
|
734
|
-
-
|
|
392
|
+
- **Timeouts are enforced** in browsers and Node.js. In 1.x the 30 s default was silently ignored, so slow requests never failed. For long downloads, set `timeoutMs` higher, or to `0` to disable it.
|
|
393
|
+
- **`WasmClient` JSON bodies are plain objects**, not `Map`s: use `body.title`, not `body.get('title')`. JSON with integers above 2^53 no longer fails the request.
|
|
394
|
+
- **More responses are decoded:** `application/*+json` (e.g. `problem+json`) is parsed as JSON instead of returned as bytes, and empty bodies are `null`.
|
|
395
|
+
- **Errors are real `Error` objects.** `kind`, `message`, `status` and the other fields are unchanged, and `name` equals `kind`. Like any `Error`, `message` isn't enumerable, so `JSON.stringify(error)` leaves it out.
|
|
396
|
+
- **Invalid `headers` values reject** with an `InvalidInputError`; they used to be dropped silently. Numbers and booleans are converted to strings.
|
|
397
|
+
- **Text is decoded as UTF-8**, whatever the `charset` in `Content-Type`. Invalid bytes become `๏ฟฝ`.
|
|
398
|
+
- **The package uses an `exports` map.** Import `rust-fetch`, `rust-fetch/rust_fetch.js` or `rust-fetch/rust_fetch_bg.wasm`.
|
|
399
|
+
- **Node.js 20+ is required.** In Node.js you no longer need to load the `.wasm` file yourself; `init()` is optional there.
|
|
735
400
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
401
|
+
These changed too, but are unlikely to break anything:
|
|
402
|
+
- POST and PATCH aren't retried by default (`retryNonIdempotent: true` re-enables it).
|
|
403
|
+
- Your `Content-Type` is no longer duplicated.
|
|
404
|
+
- `fetch_json` parses JSON whatever the `Content-Type`.
|
|
405
|
+
- Invalid URLs, and `TRACE`/`CONNECT`, fail fast with an `InvalidInputError`.
|
|
740
406
|
|
|
741
|
-
|
|
407
|
+
Rust crate users:
|
|
408
|
+
- `RetryConfig` has a new field, so add `..Default::default()` to struct literals.
|
|
409
|
+
- `Method::to_reqwest()` returns reqwest 0.12's `Method`.
|
|
410
|
+
- `RequestBuilder::header()` replaces a default header with the same name instead of adding a second value.
|
|
742
411
|
|
|
743
|
-
|
|
412
|
+
1.x keeps receiving critical fixes on the [`v1.x`](https://github.com/asemqaffaf/rust-fetch/tree/v1.x) branch (`npm install rust-fetch@1`).
|
|
744
413
|
|
|
745
|
-
|
|
746
|
-
2. Create your feature branch
|
|
747
|
-
```bash
|
|
748
|
-
git checkout -b feature/amazing-feature
|
|
749
|
-
```
|
|
750
|
-
3. Commit your changes
|
|
751
|
-
```bash
|
|
752
|
-
git commit -m 'Add some amazing feature'
|
|
753
|
-
```
|
|
754
|
-
4. Push to the branch
|
|
755
|
-
```bash
|
|
756
|
-
git push origin feature/amazing-feature
|
|
757
|
-
```
|
|
758
|
-
5. Open a Pull Request
|
|
414
|
+
## Development
|
|
759
415
|
|
|
760
|
-
|
|
416
|
+
The npm package is built from `wasm-module/` with [wasm-pack](https://rustwasm.github.io/wasm-pack/). The repository root is a private workspace.
|
|
761
417
|
|
|
762
418
|
```bash
|
|
763
|
-
#
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
#
|
|
767
|
-
npm
|
|
768
|
-
|
|
769
|
-
#
|
|
770
|
-
npm run build:wasm
|
|
419
|
+
npm run build # wasm-pack build + package metadata (wasm-module/pkg)
|
|
420
|
+
npm test # Rust unit + integration tests, then the JS tests
|
|
421
|
+
npm run test:js # JS end-to-end tests against the built package (requires a build)
|
|
422
|
+
npm run bench # rust-fetch vs native fetch (requires a build)
|
|
423
|
+
npm run size # size budget for the built package
|
|
424
|
+
npm run api:check # public TypeScript API vs the committed snapshot
|
|
425
|
+
npm run coverage:js # Node.js transport coverage (100% lines and functions)
|
|
771
426
|
|
|
772
|
-
|
|
773
|
-
|
|
427
|
+
cd wasm-module
|
|
428
|
+
wasm-pack test --node # wasm unit tests for the JS bindings
|
|
429
|
+
cargo llvm-cov --ignore-filename-regex 'src/(wasm/|http\.rs)|tests/' # coverage
|
|
774
430
|
```
|
|
775
431
|
|
|
776
|
-
|
|
432
|
+
| Suite | Where | What |
|
|
433
|
+
| --- | --- | --- |
|
|
434
|
+
| Rust unit tests | `wasm-module/src/**` | URL handling, retry policy, decoding, errors, multipart |
|
|
435
|
+
| Rust integration tests | `wasm-module/tests/integration_tests.rs` | Real HTTP against a local server: retries, timeouts, redirects, chunked/truncated/large bodies, streaming, progress, concurrency |
|
|
436
|
+
| Wasm unit tests | `wasm-module/src/wasm/tests.rs` | JS conversions, bodies, options, interceptors, timers and abort signals in Node.js |
|
|
437
|
+
| JS end-to-end tests | `wasm-module/js/test/client.test.mjs` | The built package against a local server |
|
|
438
|
+
| Package tests | `wasm-module/js/test/package.test.mjs` | Installs the packed tarball and uses it via `import`, `require`, the browser entry and its types |
|
|
777
439
|
|
|
778
|
-
|
|
440
|
+
CI runs the Rust suites with a coverage gate that fails if line or function coverage of the Rust core, or of the Node.js transport, drops below 100%. It also runs the wasm and JS suites on Node 20, 22 and 24, and checks formatting, dependencies (security advisories and licenses), API compatibility and the size budget. See [CONTRIBUTING.md](CONTRIBUTING.md#pull-requests). The JS-only bindings in `src/wasm/` and `src/http.rs` need a JS engine, so they're covered by the wasm and JS suites instead.
|
|
779
441
|
|
|
780
|
-
|
|
442
|
+
Releases are published by CI when a version tag is pushed. See [CONTRIBUTING.md](CONTRIBUTING.md#releasing).
|
|
781
443
|
|
|
782
|
-
|
|
783
|
-
- Uses [reqwest](https://github.com/seanmonstar/reqwest)
|
|
784
|
-
- Inspired by the Rust and WebAssembly community
|
|
444
|
+
## License
|
|
785
445
|
|
|
786
|
-
|
|
446
|
+
Licensed under either of [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option.
|
|
787
447
|
|
|
788
|
-
|
|
789
|
-
- Uses [reqwest](https://github.com/seanmonstar/reqwest) for HTTP operations
|
|
790
|
-
- Inspired by the Rust and WebAssembly community
|
|
791
|
-
|
|
792
|
-
## Roadmap
|
|
793
|
-
|
|
794
|
-
- [x] Add support for POST, PUT, DELETE methods โ
|
|
795
|
-
- [x] Implement request headers customization โ
|
|
796
|
-
- [x] Add comprehensive test suite โ
|
|
797
|
-
- [ ] Add streaming support for large responses
|
|
798
|
-
- [ ] Implement request cancellation (AbortController support)
|
|
799
|
-
- [ ] Performance benchmarks against native fetch
|
|
800
|
-
- [ ] Support for multipart form data and file uploads
|
|
801
|
-
- [ ] WebSocket support
|
|
802
|
-
- [ ] Request/response interceptors
|
|
803
|
-
- [ ] Connection pooling configuration
|
|
804
|
-
- [ ] Proxy support
|
|
805
|
-
- [ ] Progress tracking for uploads/downloads
|
|
806
|
-
- [ ] HTTP/2 support
|
|
448
|
+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the development and release process, and [SECURITY.md](SECURITY.md) to report a vulnerability.
|