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/README.md CHANGED
@@ -1,806 +1,448 @@
1
1
  # rust-fetch
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/rust-fetch.svg)](https://www.npmjs.com/package/rust-fetch)
4
- [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](LICENSE)
4
+ [![CI](https://github.com/asemqaffaf/rust-fetch/actions/workflows/rust.yml/badge.svg)](https://github.com/asemqaffaf/rust-fetch/actions/workflows/rust.yml)
5
+ [![Coverage: 100% (enforced)](https://img.shields.io/badge/coverage-100%25%20enforced-brightgreen.svg)](https://github.com/asemqaffaf/rust-fetch/actions/workflows/rust.yml)
6
+ [![codecov](https://codecov.io/gh/asemqaffaf/rust-fetch/branch/main/graph/badge.svg)](https://codecov.io/gh/asemqaffaf/rust-fetch)
7
+ [![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)
5
8
  [![WebAssembly](https://img.shields.io/badge/WebAssembly-654FF0?style=flat&logo=WebAssembly&logoColor=white)](https://webassembly.org/)
6
- [![Current Version](https://img.shields.io/badge/version-1.1.0-green.svg)](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
- ```bash
61
- # Using npm
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
- // Initialize WASM module
83
- await init();
84
-
85
- // Create a client instance
86
- const client = new WasmClient();
13
+ ```js
14
+ import { WasmClient } from 'rust-fetch';
87
15
 
88
- // Make requests
89
- const data = await client.get('https://api.example.com/users');
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
- ### Common Patterns
94
-
95
- #### Making GET Requests
96
- ```typescript
97
- // Simple GET
98
- const users = await client.get('/users');
20
+ ## Contents
99
21
 
100
- // With query parameters
101
- const activeUsers = await client.get('/users', {
102
- params: { status: 'active', page: 1 }
103
- });
104
-
105
- // With headers
106
- const user = await client.get('/users/1', {
107
- headers: { 'Authorization': 'Bearer token' }
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
- #### Working with POST Requests
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
- // Modern way to POST form data
120
- const response = await client.post('/upload', {
121
- body: formData,
122
- type: 'form-data',
123
- headers: { 'Content-Type': 'multipart/form-data' }
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
- // Legacy way (still supported for backward compatibility)
127
- const legacyResponse = await client.post_json('/users', {
128
- name: 'John Doe',
129
- email: 'john@example.com'
130
- });
131
- ```
48
+ ## Installation
132
49
 
133
- #### Error Handling
134
- ```typescript
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
- ## ๏ฟฝ Migration Guide
54
+ ## Quick start
148
55
 
149
- If you're upgrading from v1.0.x to v1.1.0, here's how to migrate from legacy APIs to the modern ones:
56
+ **Node.js** (20+): the WebAssembly module loads on import.
150
57
 
151
- ### Legacy to Modern API Examples
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
- // Error handling
179
- try { // Legacy
180
- const data = await fetch_json('/api/data');
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
- ### Why Migrate?
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
- ```javascript
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
- #### POST Request with JSON
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
- #### PUT Request
230
- Update a resource with PUT:
77
+ ## Guide
231
78
 
232
- ```javascript
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
- #### Custom Request with Headers
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
- // Base URL for all requests
281
- baseUrl: 'https://api.example.com',
282
-
283
- // Default headers
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
- ### Request Interceptors
90
+ Invalid options throw an `InvalidInputError` right away, from the constructor.
312
91
 
313
- ```typescript
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
- // Add response interceptors
322
- client.addResponseInterceptor(
323
- (response) => {
324
- // Handle successful response
325
- return response.data;
326
- },
327
- (error) => {
328
- // Handle errors
329
- if (error.status === 401) {
330
- return refreshTokenAndRetry(error.config);
331
- }
332
- throw error;
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
- ### Advanced Request Patterns
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
- ```typescript
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
- // Request cancellation
347
- const controller = new AbortController();
348
- const promise = client.get('/long-operation', {
349
- signal: controller.signal
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
- // Cancel after 5 seconds
353
- setTimeout(() => controller.abort(), 5000);
121
+ ### Request bodies
354
122
 
355
- // Request streaming
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
- ### Advanced Request Configuration
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
- ```javascript
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
- const url = 'https://jsonplaceholder.typicode.com/posts';
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
- fetch_with_options(url, method, headers, body)
382
- .then(response => console.log(response))
383
- .catch(error => console.error('Error:', error));
138
+ Successful (2xx) requests resolve to:
384
139
 
385
- // Modern equivalent using WasmClient
386
- const client = new WasmClient();
387
- const response = await client.request({
388
- method: 'POST',
389
- url: '/posts',
390
- headers: { 'Content-Type': 'application/json' },
391
- body: { userId: 1, title: 'foo', body: 'bar' }
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
- ## ๐Ÿ—๏ธ Architecture
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
- The library is organized into several modules:
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
- - **`error`**: Comprehensive error types with detailed context
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
- ### Error Handling
161
+ Every rejection is an `Error` whose `name` and `kind` are one of:
405
162
 
406
- The library provides rich error types that preserve context and detailed error information:
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
- ```typescript
173
+ ```js
409
174
  try {
410
- const data = await client.get('https://api.example.com/users');
411
- console.log('Success:', data);
175
+ await client.get('/users/9');
412
176
  } catch (error) {
413
- if (error instanceof NetworkError) {
414
- console.error('Network issue:', error.message);
415
- // Access additional context
416
- console.log('Attempted URL:', error.url);
417
- console.log('Duration:', error.duration);
418
- } else if (error instanceof HttpError) {
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
- ### TypeScript Support
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
- The library includes comprehensive TypeScript definitions for type-safe API usage:
189
+ ### Timeouts
430
190
 
431
- ```typescript
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
- interface User {
435
- id: number;
436
- name: string;
437
- email: string;
438
- }
439
-
440
- const client = new WasmClient();
193
+ ### Retries
441
194
 
442
- // Type-safe request and response
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
- // Type-safe request body
446
- const newUser: User = await client.post_json<User>('/users', {
447
- name: 'John Doe',
448
- email: 'john@example.com'
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
- ## API Reference
462
-
463
- ### WasmClient Class
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
- The `WasmClient` class provides a rich, fluent interface for making HTTP requests with full type safety and advanced configuration options.
216
+ ### Cancellation
466
217
 
467
- #### Creating a Client
468
-
469
- ```javascript
470
- import init, { WasmClient } from 'rust-fetch';
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
- // With custom configuration
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
- #### HTTP Methods
226
+ Aborting cancels the request in flight, any pending retry delay, and a body being streamed.
484
227
 
485
- All HTTP methods support type-safe request and response handling:
228
+ ### Streaming
486
229
 
487
- ##### GET Request
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
- ##### POST Request with JSON
497
- ```javascript
498
- const response = await client.post_json('/users', {
499
- name: 'John Doe',
500
- email: 'john@example.com'
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
- ##### PUT Request
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
- ##### DELETE Request
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
- ##### PATCH Request
522
- ```javascript
523
- const response = await client.patch_json('/users/1', {
524
- email: 'new.email@example.com'
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
- ##### HEAD Request
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
- ### Legacy Utility Functions
254
+ Upload progress isn't available, because `fetch()` doesn't report it.
555
255
 
556
- > **Deprecation Notice**: These standalone functions are maintained for backward compatibility with v1.0.x. For new projects, we recommend using the `WasmClient` class which provides a more robust and type-safe API.
256
+ ### File uploads
557
257
 
558
- #### `fetch_json(url: string): Promise<any>` (Legacy)
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
- // Modern equivalent
565
- const client = new WasmClient();
566
- const data = await client.get('https://api.example.com/data', { type: 'json' });
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
- #### `fetch_text(url: string): Promise<string>` (Legacy)
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
- // Modern equivalent
576
- const client = new WasmClient();
577
- const html = await client.get('https://example.com', { type: 'text' });
578
- ```
269
+ ### Interceptors
579
270
 
580
- #### `fetch_with_options(url: string, method: string, headers: object, body?: string): Promise<object>` (Legacy)
581
- ```javascript
582
- import { fetch_with_options } from 'rust-fetch';
583
-
584
- const response = await fetch_with_options(
585
- 'https://api.example.com/users',
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
- #### `client.get(url: string): Promise<object>`
593
- Performs a GET request.
278
+ // Resolve to the body instead of the whole response
279
+ client.addResponseInterceptor((response) => response.body);
594
280
 
595
- ```javascript
596
- const response = await client.get('https://api.example.com/data');
281
+ // Report failures, then keep rejecting
282
+ client.addResponseInterceptor(undefined, (error) => {
283
+ reportError(error);
284
+ throw error;
285
+ });
597
286
  ```
598
287
 
599
- #### `client.post_json(url: string, body: object): Promise<object>`
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
- Performs a POST request with JSON body.
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
- ```javascript
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
- // Recommended modern way
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
- #### `client.put_json(url: string, body: object): Promise<object>`
619
- Performs a PUT request with JSON body.
299
+ ```ts
300
+ import { WasmClient, type HttpResponse, type RustFetchError } from 'rust-fetch';
620
301
 
621
- ```javascript
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
- #### `client.delete(url: string): Promise<object>`
629
- Performs a DELETE request.
304
+ const res: HttpResponse<User> = await client.get('/users/42');
305
+ res.body.name; // string
630
306
 
631
- ```javascript
632
- const response = await client.delete('https://api.example.com/users/1');
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
- #### `client.patch_json(url: string, body: object): Promise<object>`
636
- Performs a PATCH request with JSON body.
315
+ ## Legacy functions
637
316
 
638
- ```javascript
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
- #### `client.head(url: string): Promise<object>`
645
- Performs a HEAD request to check resource metadata.
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
- ```javascript
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
- #### `client.request(method: string, url: string, headers?: object, body?: string | object): Promise<object>`
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
- ```javascript
656
- const response = await client.request(
657
- 'POST',
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
- ## โšก Performance
341
+ let user = client.get("users/42").await?;
342
+ println!("{}", user.json().unwrap()["name"]);
666
343
 
667
- ### Benchmarks
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
- ```plaintext
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
- ### Optimization Tips
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
- - **Initial Load**: Cache the WASM module for faster subsequent loads
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
- ### Browser Support
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
- | Browser | Minimum Version |
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
- ## ๐Ÿ” Troubleshooting
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
- ### Common Issues
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
- #### WASM Module Not Loading
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
- ```typescript
706
- // Correct initialization
707
- await init();
708
- const client = new WasmClient();
370
+ ## Roadmap
709
371
 
710
- // Incorrect - will fail
711
- const client = new WasmClient();
712
- await init();
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
- #### CORS Issues
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
- ```typescript
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
- #### Node.js Integration
732
- - โœ… Check package installation
733
- - โœ… Verify WASM file location
734
- - โœ… Set proper Node.js flags if needed
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
- ```bash
737
- # Correct Node.js flags
738
- node --experimental-wasm-modules your-app.js
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
- ## ๐Ÿค Contributing
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
- We welcome contributions! Here's how you can help:
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
- 1. Fork the repository
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
- ### Development Setup
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
- # Clone the repository
764
- git clone https://github.com/asemqaffaf/rust-fetch.git
765
-
766
- # Install dependencies
767
- npm install
768
-
769
- # Build WASM module
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
- # Run tests
773
- npm test
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
- ## ๐Ÿ“ License
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
- This project is licensed under the ISC License - see the [LICENSE](LICENSE) file for details.
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
- ## ๐Ÿ™ Acknowledgments
442
+ Releases are published by CI when a version tag is pushed. See [CONTRIBUTING.md](CONTRIBUTING.md#releasing).
781
443
 
782
- - Built with [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen)
783
- - Uses [reqwest](https://github.com/seanmonstar/reqwest)
784
- - Inspired by the Rust and WebAssembly community
444
+ ## License
785
445
 
786
- ## Acknowledgments
446
+ Licensed under either of [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option.
787
447
 
788
- - Built with [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen)
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.