@autofleet/rapido-http-client 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +780 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +151 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.cjs +2 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +148 -0
- package/dist/testing.d.ts +148 -0
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -0
- package/dist/utils-B1ZhSXeE.cjs +2 -0
- package/dist/utils-B1ZhSXeE.cjs.map +1 -0
- package/dist/utils-B2KjeMp7.js +2 -0
- package/dist/utils-B2KjeMp7.js.map +1 -0
- package/package.json +73 -0
package/README.md
ADDED
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
# @autofleet/rapido-http-client
|
|
2
|
+
|
|
3
|
+
> [!TIP]
|
|
4
|
+
> TL;DR - Modern, high-performance HTTP client built on [undici](https://github.com/nodejs/undici) - Node.js's official modern HTTP client.
|
|
5
|
+
|
|
6
|
+
**Rapido HTTP Client** is a high-performance, type-safe HTTP client for Node.js applications, built on top of [undici](https://github.com/nodejs/undici). It offers advanced features like automatic retries, caching, schema validation with Zod, and seamless integration with service discovery systems.
|
|
7
|
+
|
|
8
|
+
## Why Rapido HTTP Client?
|
|
9
|
+
|
|
10
|
+
Rapido HTTP Client is the next-generation replacement for `@autofleet/network`, designed from the ground up for modern Node.js applications:
|
|
11
|
+
### Key Advantages Over @autofleet/network
|
|
12
|
+
|
|
13
|
+
| Feature | Rapido HTTP Client (undici) | Network (axios) |
|
|
14
|
+
|---------|-------------------|-----------------|
|
|
15
|
+
| **Performance** | ✅ Undici by Node.js, nearly 200x faster | ❌ Axios is not optimized for speed |
|
|
16
|
+
| **Modern APIs** | ✅ Built-in caching, retry interceptors | ❌ Requires plugins |
|
|
17
|
+
| **Type Safety** | ✅ Full TypeScript + Opt-in Zod validation | ⚠️ Only type casting (with generics) |
|
|
18
|
+
| **Memory Efficiency** | ✅ Connection pooling | ⚠️ No pooling, slower connections |
|
|
19
|
+
| **Maintenance** | ✅ Official Node.js project | ⚠️ Unmaintained version of Axios (0.x) |
|
|
20
|
+
|
|
21
|
+
### Performance Benchmarks
|
|
22
|
+
|
|
23
|
+
Benchmarks comparing Rapido HTTP Client (undici) vs Network (axios):
|
|
24
|
+
|
|
25
|
+
| Operation | Rapido HTTP Client (ops/sec) | Network (ops/sec) | **Performance Gain** |
|
|
26
|
+
|-----------|---------------------|-------------------|----------------------|
|
|
27
|
+
| Initialization | 1,173,118 | 60,947 | **19.2x faster** ⚡ |
|
|
28
|
+
| Simple GET | 61,916 | 319 | **194x faster** ⚡ |
|
|
29
|
+
| POST with body | 61,459 | 334 | **184x faster** ⚡ |
|
|
30
|
+
| GET with params | 58,347 | 270 | **216x faster** ⚡ |
|
|
31
|
+
| PUT request | 64,213 | 349 | **184x faster** ⚡ |
|
|
32
|
+
| DELETE request | 111,402 | 624 | **178x faster** ⚡ |
|
|
33
|
+
| With Zod schema | 58,860 | 289 | **204x faster** ⚡ |
|
|
34
|
+
| getAllPages | 20,654 | 119 | **173x faster** ⚡ |
|
|
35
|
+
| With retries | 827 | 146 | **5.7x faster** ⚡ |
|
|
36
|
+
|
|
37
|
+
**Rapido HTTP Client is 170-200x faster** than Network for most operations, especially in happy-paths!
|
|
38
|
+
|
|
39
|
+
Take into account that these benchmarks are run in ideal conditions - no actual network latency. In real-world scenarios, the performance gap may be even larger due to Rapido HTTP Client's efficient connection pooling and lower overhead.
|
|
40
|
+
It might also become smaller in high-latency networks, where network time dominates processing time.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm i @autofleet/rapido-http-client
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Optional Dependencies
|
|
49
|
+
|
|
50
|
+
For schema validation support:
|
|
51
|
+
```bash
|
|
52
|
+
npm i zod
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
<details>
|
|
58
|
+
<summary><b>Basic Usage</b></summary>
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import RapidoHttpClient from '@autofleet/rapido-http-client';
|
|
62
|
+
import { logger } from '@autofleet/logger';
|
|
63
|
+
|
|
64
|
+
const client = new RapidoHttpClient({
|
|
65
|
+
logger,
|
|
66
|
+
serviceUrl: 'https://api.example.com',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Simple GET request
|
|
70
|
+
const { data, status } = await client.get('/users');
|
|
71
|
+
|
|
72
|
+
// POST with body
|
|
73
|
+
const response = await client.post('/users', {
|
|
74
|
+
name: 'John Doe',
|
|
75
|
+
email: 'john@example.com',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Clean up when done
|
|
79
|
+
await client.close();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
</details>
|
|
83
|
+
|
|
84
|
+
<details>
|
|
85
|
+
<summary><b>With Service Discovery (Kubernetes)</b></summary>
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
const client = new RapidoHttpClient({
|
|
89
|
+
logger,
|
|
90
|
+
serviceName: 'USER', // Reads from USER_SERVICE_HOST env var
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const { data } = await client.get('/profile');
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
</details>
|
|
97
|
+
|
|
98
|
+
<details>
|
|
99
|
+
<summary><b>With Retry Logic</b></summary>
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const client = new RapidoHttpClient({
|
|
103
|
+
serviceUrl: 'https://api.example.com',
|
|
104
|
+
logger,
|
|
105
|
+
retries: {
|
|
106
|
+
maxRetries: 3,
|
|
107
|
+
minTimeout: 100,
|
|
108
|
+
timeoutFactor: 2, // Exponential backoff
|
|
109
|
+
statusCodes: [429, 500, 502, 503], // Retry on these status codes
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Automatically retries on failure
|
|
114
|
+
const { data } = await client.get('/flaky-endpoint');
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
</details>
|
|
118
|
+
|
|
119
|
+
<details>
|
|
120
|
+
<summary><b>With HTTP Caching</b></summary>
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const client = new RapidoHttpClient({
|
|
124
|
+
serviceUrl: 'https://api.example.com',
|
|
125
|
+
logger,
|
|
126
|
+
cache: {
|
|
127
|
+
maxCount: 1000, // Max cached responses
|
|
128
|
+
maxSize: 10 * 1024 * 1024, // 10MB cache size
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// First request hits network
|
|
133
|
+
const response1 = await client.get('/data');
|
|
134
|
+
|
|
135
|
+
// Second request returns from cache (if cache-control headers permit)
|
|
136
|
+
const response2 = await client.get('/data');
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
</details>
|
|
140
|
+
|
|
141
|
+
<details>
|
|
142
|
+
<summary><b>With Zod Schema Validation</b></summary>
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { z } from 'zod';
|
|
146
|
+
|
|
147
|
+
const UserSchema = z.object({
|
|
148
|
+
id: z.number(),
|
|
149
|
+
name: z.string(),
|
|
150
|
+
email: z.string().email(),
|
|
151
|
+
role: z.enum(['admin', 'user']),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Type is automatically inferred from schema!
|
|
155
|
+
const { data } = await client.get('/user/123', {
|
|
156
|
+
responseSchema: UserSchema,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// TypeScript knows: data is { id: number; name: string; email: string; role: 'admin' | 'user' }
|
|
160
|
+
console.log(data.name); // ✅ Type-safe & Runtime-validated!
|
|
161
|
+
|
|
162
|
+
// Throws ZodError if response doesn't match schema
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
</details>
|
|
166
|
+
|
|
167
|
+
<details>
|
|
168
|
+
<summary><b>Request Timeouts & Cancellation</b></summary>
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// Global timeout for all requests
|
|
172
|
+
const client = new RapidoHttpClient({
|
|
173
|
+
logger,
|
|
174
|
+
timeout: 5_000, // 5 seconds
|
|
175
|
+
serviceUrl: 'https://api.example.com',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Override timeout per request
|
|
179
|
+
await client.get('/slow-endpoint', { timeout: 10_000 });
|
|
180
|
+
|
|
181
|
+
// Manual cancellation with AbortSignal
|
|
182
|
+
try {
|
|
183
|
+
await client.get('/data', { signal: AbortSignal.timeout(3_000) });
|
|
184
|
+
} catch (error) {
|
|
185
|
+
// RequestAbortedError thrown when cancelled
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
</details>
|
|
190
|
+
|
|
191
|
+
<details>
|
|
192
|
+
<summary><b>Query Parameters</b></summary>
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// Simple params
|
|
196
|
+
await client.get('/search', {
|
|
197
|
+
params: { q: 'nodejs', limit: 10 },
|
|
198
|
+
});
|
|
199
|
+
// → GET /search?q=nodejs&limit=10
|
|
200
|
+
|
|
201
|
+
// Array params (auto-formatted with [])
|
|
202
|
+
await client.get('/filter', {
|
|
203
|
+
params: { tags: ['typescript', 'nodejs'] },
|
|
204
|
+
});
|
|
205
|
+
// → GET /filter?tags[]=typescript&tags[]=nodejs
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
</details>
|
|
209
|
+
|
|
210
|
+
<details>
|
|
211
|
+
<summary><b>Custom Headers & Outbreak Tracing</b></summary>
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
const client = new RapidoHttpClient({
|
|
215
|
+
serviceUrl: 'https://api.example.com',
|
|
216
|
+
logger,
|
|
217
|
+
headers: {
|
|
218
|
+
'X-API-Key': 'secret-key',
|
|
219
|
+
'X-Custom-Header': 'value',
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Outbreak trace headers are automatically injected
|
|
224
|
+
// from the current context (x-trace-id, x-correlation-id, etc.)
|
|
225
|
+
await client.get('/data');
|
|
226
|
+
|
|
227
|
+
// Override headers per request
|
|
228
|
+
await client.get('/data', {
|
|
229
|
+
headers: { 'X-Request-Specific': 'value' },
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
</details>
|
|
234
|
+
|
|
235
|
+
<details>
|
|
236
|
+
<summary><b>Pagination Helpers</b></summary>
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// Fetch all pages from a paginated GET endpoint
|
|
240
|
+
const allItems = await client.getAllPages<Item>('/api/items');
|
|
241
|
+
// Automatically fetches pages until no more data
|
|
242
|
+
|
|
243
|
+
// Fetch all pages from a POST query endpoint
|
|
244
|
+
const allResults = await client.getAllPagesFromQueryEndpoint<Result>(
|
|
245
|
+
'/api/query',
|
|
246
|
+
{ filter: 'active' }, // Additional query parameters
|
|
247
|
+
100 // Max pages (default: 100, -1 for unlimited)
|
|
248
|
+
);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
</details>
|
|
252
|
+
|
|
253
|
+
<details>
|
|
254
|
+
<summary><b>Async Disposal (Node.js 20+)</b></summary>
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// Automatic cleanup with async using
|
|
258
|
+
{
|
|
259
|
+
await using client = new RapidoHttpClient({
|
|
260
|
+
serviceUrl: 'https://api.example.com',
|
|
261
|
+
logger,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await client.get('/data');
|
|
265
|
+
|
|
266
|
+
// client.close() called automatically when scope exits
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
</details>
|
|
271
|
+
|
|
272
|
+
<details>
|
|
273
|
+
<summary><b>Automatic Response Decompression</b></summary>
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// Enable automatic decompression of gzip, brotli, deflate, and zstd responses
|
|
277
|
+
const client = new RapidoHttpClient({
|
|
278
|
+
logger,
|
|
279
|
+
autoDecompress: true,
|
|
280
|
+
serviceUrl: 'https://api.example.com',
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Automatically decompresses compressed responses
|
|
284
|
+
// Sets Accept-Encoding header to: zstd, br, gzip (or br, gzip if Node version is lacking zstd support)
|
|
285
|
+
const { data } = await client.get('/data');
|
|
286
|
+
|
|
287
|
+
// Works with both success and error responses
|
|
288
|
+
// No need to manually handle content-encoding headers
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
</details>
|
|
292
|
+
|
|
293
|
+
## Compression Support
|
|
294
|
+
|
|
295
|
+
Rapido HTTP Client supports automatic decompression of compressed HTTP responses through the `autoDecompress` option.
|
|
296
|
+
|
|
297
|
+
### Enabling Auto-Decompression
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
const client = new RapidoHttpClient({
|
|
301
|
+
logger,
|
|
302
|
+
serviceUrl: 'https://api.example.com',
|
|
303
|
+
autoDecompress: true,
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### What It Does
|
|
308
|
+
|
|
309
|
+
When `autoDecompress: true` is enabled:
|
|
310
|
+
|
|
311
|
+
1. **Sets Accept-Encoding Header**: Automatically adds the `Accept-Encoding` header to advertise supported compression formats:
|
|
312
|
+
- **Node.js 22+**: `zstd, br, gzip` (includes zstd support)
|
|
313
|
+
- **Node.js < 22**: `br, gzip` (zstd not supported in older versions)
|
|
314
|
+
|
|
315
|
+
2. **Automatic Decompression**: Transparently decompresses responses with the following `content-encoding` values:
|
|
316
|
+
- `gzip` - GZIP compression
|
|
317
|
+
- `br` - Brotli compression
|
|
318
|
+
- `deflate` - Deflate compression
|
|
319
|
+
- `zstd` - Zstandard compression (Node.js 22+ only)
|
|
320
|
+
|
|
321
|
+
3. **Works for All Responses**: Handles both successful responses and error responses (4xx, 5xx)
|
|
322
|
+
|
|
323
|
+
### Example Usage
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
const client = new RapidoHttpClient({
|
|
327
|
+
logger,
|
|
328
|
+
serviceUrl: 'https://api.example.com',
|
|
329
|
+
autoDecompress: true,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Server responds with gzipped JSON and content-encoding: gzip
|
|
333
|
+
const { data } = await client.get('/data');
|
|
334
|
+
|
|
335
|
+
// Rapido HTTP Client automatically decompresses the response
|
|
336
|
+
// You receive the parsed JSON object directly
|
|
337
|
+
console.log(data); // { ... } - already decompressed and parsed
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### When to Use
|
|
341
|
+
|
|
342
|
+
Enable `autoDecompress` when:
|
|
343
|
+
- Communicating with external APIs that send compressed responses
|
|
344
|
+
- Optimizing bandwidth usage with servers that support compression
|
|
345
|
+
- Working with CDNs or services that automatically compress responses
|
|
346
|
+
|
|
347
|
+
### When Not to Use
|
|
348
|
+
|
|
349
|
+
Leave `autoDecompress` disabled (default) when:
|
|
350
|
+
- Internal microservice communication where responses aren't compressed (at least at the time of writing this)
|
|
351
|
+
- You need to handle raw compressed data yourself
|
|
352
|
+
- The overhead of checking and decompressing isn't worth the bandwidth savings
|
|
353
|
+
|
|
354
|
+
### Error Handling
|
|
355
|
+
|
|
356
|
+
Decompression works transparently for error responses too:
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
try {
|
|
360
|
+
await client.get('/api/endpoint');
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (error instanceof errors.RapidoHttpClientError) {
|
|
363
|
+
// error.data is already decompressed, even if the server
|
|
364
|
+
// sent a compressed 4xx/5xx response
|
|
365
|
+
console.log(error.data);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## API Reference
|
|
371
|
+
|
|
372
|
+
### HTTP Methods
|
|
373
|
+
|
|
374
|
+
All methods return `Promise<RapidoHttpClientResponse<T>>`:
|
|
375
|
+
|
|
376
|
+
- `get<T>(url, config?)` - GET request
|
|
377
|
+
- `post<T>(url, body?, config?)` - POST request
|
|
378
|
+
- `put<T>(url, body?, config?)` - PUT request
|
|
379
|
+
- `patch<T>(url, body?, config?)` - PATCH request
|
|
380
|
+
- `delete<T>(url, config?)` - DELETE request
|
|
381
|
+
- `head<T>(url, config?)` - HEAD request
|
|
382
|
+
- `options<T>(url, config?)` - OPTIONS request
|
|
383
|
+
- `query<T>(url, body?, config?)` - QUERY request
|
|
384
|
+
|
|
385
|
+
### Request Config
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
interface RequestConfig {
|
|
389
|
+
params?: Record<string, any>; // Query parameters
|
|
390
|
+
headers?: Record<string, string>;// Request headers
|
|
391
|
+
timeout?: number; // Override timeout
|
|
392
|
+
signal?: AbortSignal; // Cancellation signal
|
|
393
|
+
responseSchema?: ZodType; // Response validation schema
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Response Format
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
interface RapidoHttpClientResponse<T> {
|
|
401
|
+
data: T; // Parsed response body
|
|
402
|
+
status: number; // HTTP status code
|
|
403
|
+
statusText: string; // Status text
|
|
404
|
+
headers: Record<string, string | string[]>; // Response headers
|
|
405
|
+
config: { // Request metadata
|
|
406
|
+
url: string;
|
|
407
|
+
method: string;
|
|
408
|
+
baseURL?: string;
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Testing
|
|
414
|
+
|
|
415
|
+
Rapido HTTP Client provides a simple, nock-like testing API that allows you to mock HTTP responses without having to change how you instantiate your client.
|
|
416
|
+
|
|
417
|
+
This mock API only mocks the network layer, so your Rapido HTTP Client instances are created normally - no special test parameters are needed, while all features (retries, caching, schema validation) work as expected.
|
|
418
|
+
|
|
419
|
+
### Quick Start
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
import { setupRapidoHttpClientMock } from '@autofleet/rapido-http-client/testing';
|
|
423
|
+
import RapidoHttpClient from '@autofleet/rapido-http-client';
|
|
424
|
+
|
|
425
|
+
describe('My Service Tests', () => {
|
|
426
|
+
let mock: ReturnType<typeof setupRapidoHttpClientMock>;
|
|
427
|
+
|
|
428
|
+
beforeAll(() => {
|
|
429
|
+
// Set up the global mock
|
|
430
|
+
mock = setupRapidoHttpClientMock();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
afterAll(async () => {
|
|
434
|
+
// Clean up after each test
|
|
435
|
+
await mock.cleanup();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('should fetch users', async () => {
|
|
439
|
+
// Set up mock responses
|
|
440
|
+
mock.intercept('https://api.example.com')
|
|
441
|
+
.get('/users')
|
|
442
|
+
.reply(200, { users: [{ id: 1, name: 'John' }] }, {
|
|
443
|
+
headers: { 'content-type': 'application/json' }
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Create your client normally - no special parameters needed!
|
|
447
|
+
const client = new RapidoHttpClient({
|
|
448
|
+
serviceUrl: 'https://api.example.com',
|
|
449
|
+
logger: mockLogger,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Make requests - they automatically use the mocks
|
|
453
|
+
const response = await client.get('/users');
|
|
454
|
+
|
|
455
|
+
expect(response.data).toEqual({ users: [{ id: 1, name: 'John' }] });
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### API Reference
|
|
461
|
+
|
|
462
|
+
#### `setupRapidoHttpClientMock()`
|
|
463
|
+
|
|
464
|
+
Creates a global mock that is automatically used by all Rapido HTTP Client instances.
|
|
465
|
+
|
|
466
|
+
> [!IMPORTANT]
|
|
467
|
+
> The `setupRapidoHttpClientMock()` function must be called **before** creating the Rapido HTTP Client instance, so the instantiation picks up the global mock agent.
|
|
468
|
+
> This means you should typically call it in `beforeAll` hooks, and after that dynamically import the module creating the Rapido HTTP Client instance.
|
|
469
|
+
|
|
470
|
+
##### `intercept(target)`
|
|
471
|
+
|
|
472
|
+
Set up mocks for a specific origin. Accepts either a URL string or an object with `serviceName` or `serviceUrl`. Returns an instance of unici's `MockInterceptor` object. See [the docs](https://undici.nodejs.org/#/docs/api/MockPool?id=return-mockinterceptor) for details.
|
|
473
|
+
|
|
474
|
+
###### Some examples:
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
const mock = setupRapidoHttpClientMock();
|
|
478
|
+
|
|
479
|
+
// Simple path matching
|
|
480
|
+
mock.intercept('https://api.example.com')
|
|
481
|
+
.get('/users')
|
|
482
|
+
.reply(200, { users: [] }, { headers: { 'content-type': 'application/json' } });
|
|
483
|
+
|
|
484
|
+
// Advanced: Match specific request body
|
|
485
|
+
mock.intercept('https://api.example.com')
|
|
486
|
+
.post({ path: '/users', body: JSON.stringify({ name: 'Jane' }) })
|
|
487
|
+
.reply(201, { id: 2, name: 'Jane' }, { headers: { 'content-type': 'application/json' } });
|
|
488
|
+
|
|
489
|
+
// Advanced: Match query parameters
|
|
490
|
+
mock.intercept('https://api.example.com')
|
|
491
|
+
.get({ path: '/users', query: { page: '1', limit: '10' } })
|
|
492
|
+
.reply(200, { users: [], page: 1 }, { headers: { 'content-type': 'application/json' } });
|
|
493
|
+
|
|
494
|
+
// Using serviceName (reads from env variable)
|
|
495
|
+
mock.intercept({ serviceName: 'USER' })
|
|
496
|
+
.get('/profile')
|
|
497
|
+
.reply(200, { id: 1, name: 'Alice' }, { headers: { 'content-type': 'application/json' } });
|
|
498
|
+
|
|
499
|
+
// Using serviceUrl
|
|
500
|
+
mock.intercept({ serviceUrl: 'https://api.example.com' })
|
|
501
|
+
.delete('/users/1')
|
|
502
|
+
.reply(204);
|
|
503
|
+
|
|
504
|
+
// Mock for a specific number of requests
|
|
505
|
+
mock.intercept('https://api.example.com')
|
|
506
|
+
.get('/data')
|
|
507
|
+
.reply(200, { value: 1 }, { headers: { 'content-type': 'application/json' } })
|
|
508
|
+
.times(3); // Only mock the first 3 requests
|
|
509
|
+
|
|
510
|
+
// Mock indefinitely
|
|
511
|
+
mock.intercept('https://api.example.com')
|
|
512
|
+
.get('/data')
|
|
513
|
+
.reply(200, { value: 1 }, { headers: { 'content-type': 'application/json' } })
|
|
514
|
+
.persist(); // Always return this response
|
|
515
|
+
|
|
516
|
+
// Mock a network error
|
|
517
|
+
const error = new Error('ECONNREFUSED: Connection refused');
|
|
518
|
+
error.code = 'ECONNREFUSED';
|
|
519
|
+
|
|
520
|
+
mock.intercept('https://api.example.com')
|
|
521
|
+
.get('/failing-endpoint')
|
|
522
|
+
.replyWithError(error)
|
|
523
|
+
.persist(); // Always throw this error
|
|
524
|
+
|
|
525
|
+
// Mock timeout errors
|
|
526
|
+
const timeoutError = new Error('Request timeout');
|
|
527
|
+
timeoutError.code = 'UND_ERR_TIMEOUT';
|
|
528
|
+
|
|
529
|
+
mock.intercept('https://api.example.com')
|
|
530
|
+
.get('/slow-endpoint')
|
|
531
|
+
.replyWithError(timeoutError)
|
|
532
|
+
.times(2); // First 2 requests fail, then succeeds
|
|
533
|
+
|
|
534
|
+
const client = new RapidoHttpClient({
|
|
535
|
+
serviceUrl: 'https://api.example.com',
|
|
536
|
+
logger,
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// This will throw the error
|
|
540
|
+
await expect(client.get('/failing-endpoint')).rejects.toThrow('ECONNREFUSED');
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
##### `getPool(origin: string): MockPool`
|
|
544
|
+
|
|
545
|
+
Get the underlying [undici `MockPool`](https://undici.nodejs.org/#/docs/api/MockPool?id=instance-methods) for advanced usage:
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
const pool = mock.getPool('https://api.example.com');
|
|
549
|
+
|
|
550
|
+
// Use undici's full API
|
|
551
|
+
pool.intercept({
|
|
552
|
+
path: '/complex',
|
|
553
|
+
method: 'GET',
|
|
554
|
+
headers: { 'x-custom': 'value' }
|
|
555
|
+
})
|
|
556
|
+
.reply(200, { advanced: true }, { headers: { 'content-type': 'application/json' } });
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
##### `cleanup(): Promise<void>`
|
|
560
|
+
|
|
561
|
+
Clean up the global mock. **Should be called** after tests complete (typically in `afterAll`):
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
afterAll(async () => {
|
|
565
|
+
await mock.cleanup();
|
|
566
|
+
});
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### Testing Examples
|
|
570
|
+
|
|
571
|
+
<details>
|
|
572
|
+
<summary><b>Mocking Multiple Services</b></summary>
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
it('works with multiple services', async () => {
|
|
576
|
+
mock.intercept('https://api1.example.com')
|
|
577
|
+
.get('/data')
|
|
578
|
+
.reply(200, { service: 'api1' }, { headers: { 'content-type': 'application/json' } });
|
|
579
|
+
|
|
580
|
+
mock.intercept('https://api2.example.com')
|
|
581
|
+
.get('/data')
|
|
582
|
+
.reply(200, { service: 'api2' }, { headers: { 'content-type': 'application/json' } });
|
|
583
|
+
|
|
584
|
+
const client1 = new RapidoHttpClient({
|
|
585
|
+
serviceUrl: 'https://api1.example.com',
|
|
586
|
+
logger: mockLogger,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const client2 = new RapidoHttpClient({
|
|
590
|
+
serviceUrl: 'https://api2.example.com',
|
|
591
|
+
logger: mockLogger,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const response1 = await client1.get('/data');
|
|
595
|
+
const response2 = await client2.get('/data');
|
|
596
|
+
|
|
597
|
+
expect(response1.data).toEqual({ service: 'api1' });
|
|
598
|
+
expect(response2.data).toEqual({ service: 'api2' });
|
|
599
|
+
});
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
</details>
|
|
603
|
+
|
|
604
|
+
<details>
|
|
605
|
+
<summary><b>Error Responses</b></summary>
|
|
606
|
+
|
|
607
|
+
```typescript
|
|
608
|
+
it('handles error responses', async () => {
|
|
609
|
+
mock.intercept('https://api.example.com')
|
|
610
|
+
.get('/users/999')
|
|
611
|
+
.reply(404, { error: 'Not Found' }, { headers: { 'content-type': 'application/json' } });
|
|
612
|
+
|
|
613
|
+
const client = new RapidoHttpClient({
|
|
614
|
+
serviceUrl: 'https://api.example.com',
|
|
615
|
+
logger: mockLogger,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
await expect(client.get('/users/999')).rejects.toThrow();
|
|
619
|
+
});
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
</details>
|
|
623
|
+
|
|
624
|
+
<details>
|
|
625
|
+
<summary><b>Advanced `MockPool` Usage</b></summary>
|
|
626
|
+
|
|
627
|
+
For full control, use `getPool()` to access the undici `MockPool` directly:
|
|
628
|
+
|
|
629
|
+
```typescript
|
|
630
|
+
it('uses advanced pool features', async () => {
|
|
631
|
+
const pool = mock.getPool('https://api.example.com');
|
|
632
|
+
|
|
633
|
+
pool.intercept({
|
|
634
|
+
path: '/data',
|
|
635
|
+
method: 'POST',
|
|
636
|
+
body: (body) => {
|
|
637
|
+
// Custom body matching logic
|
|
638
|
+
const data = JSON.parse(body as string);
|
|
639
|
+
return data.name === 'test';
|
|
640
|
+
},
|
|
641
|
+
headers: {
|
|
642
|
+
'x-api-key': 'secret'
|
|
643
|
+
}
|
|
644
|
+
})
|
|
645
|
+
.reply(201, { created: true }, { headers: { 'content-type': 'application/json' } });
|
|
646
|
+
|
|
647
|
+
const client = new RapidoHttpClient({
|
|
648
|
+
serviceUrl: 'https://api.example.com',
|
|
649
|
+
logger: mockLogger,
|
|
650
|
+
headers: { 'x-api-key': 'secret' }
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const response = await client.post('/data', { name: 'test' });
|
|
654
|
+
expect(response.status).toBe(201);
|
|
655
|
+
});
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
</details>
|
|
659
|
+
|
|
660
|
+
### How It Works
|
|
661
|
+
|
|
662
|
+
When you call `setupRapidoHttpClientMock()`, it creates a global MockAgent that is stored in `globalThis`. The Rapido HTTP Client constructor automatically checks for this global mock agent and uses it instead of creating a real HTTP client.
|
|
663
|
+
|
|
664
|
+
This means:
|
|
665
|
+
- ✅ No need to pass special test parameters to your Rapido HTTP Client instances
|
|
666
|
+
- ✅ Your production code remains unchanged
|
|
667
|
+
- ✅ Works with any testing framework (Vitest, Jest, etc.)
|
|
668
|
+
- ✅ Clean separation between test setup and implementation
|
|
669
|
+
|
|
670
|
+
### Migration from nock
|
|
671
|
+
|
|
672
|
+
If you're migrating from `nock`, the API is very similar:
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
// Old (nock)
|
|
676
|
+
nock('https://api.example.com')
|
|
677
|
+
.get('/users')
|
|
678
|
+
.reply(200, { users: [] });
|
|
679
|
+
|
|
680
|
+
// New (rapido-http-client)
|
|
681
|
+
const mock = setupRapidoHttpClientMock();
|
|
682
|
+
mock.intercept('https://api.example.com')
|
|
683
|
+
.get('/users')
|
|
684
|
+
.reply(200, { users: [] }, { headers: { 'content-type': 'application/json' } });
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
The main differences:
|
|
688
|
+
- You need to call `mock.cleanup()` in `afterAll`
|
|
689
|
+
- Headers should be specified in the third parameter to `reply()`
|
|
690
|
+
- The mock is set up once per test file/suite
|
|
691
|
+
|
|
692
|
+
## Running Tests & Benchmarks
|
|
693
|
+
|
|
694
|
+
### Run Tests
|
|
695
|
+
|
|
696
|
+
```bash
|
|
697
|
+
# Run all tests
|
|
698
|
+
pnpm --filter @autofleet/rapido-http-client test
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### Generate Coverage Report
|
|
702
|
+
|
|
703
|
+
```bash
|
|
704
|
+
pnpm --filter @autofleet/rapido-http-client coverage
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
### Run Benchmarks
|
|
708
|
+
|
|
709
|
+
```bash
|
|
710
|
+
# Run all benchmarks
|
|
711
|
+
pnpm --filter @autofleet/rapido-http-client bench
|
|
712
|
+
|
|
713
|
+
# Run benchmarks and save results
|
|
714
|
+
pnpm --filter @autofleet/rapido-http-client bench --outputJson benchmark-results.json
|
|
715
|
+
|
|
716
|
+
# Compare with previous results
|
|
717
|
+
pnpm --filter @autofleet/rapido-http-client bench --compare benchmark-results.json
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
## Migration from @autofleet/network
|
|
721
|
+
|
|
722
|
+
### Basic Request
|
|
723
|
+
|
|
724
|
+
**Before (Network):**
|
|
725
|
+
```typescript
|
|
726
|
+
import Network from '@autofleet/network';
|
|
727
|
+
|
|
728
|
+
const network = new Network({
|
|
729
|
+
serviceUrl: 'https://api.example.com',
|
|
730
|
+
logger,
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
const response = await network.get('/data');
|
|
734
|
+
const data = response.data;
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
**After (Rapido HTTP Client):**
|
|
738
|
+
```typescript
|
|
739
|
+
import { RapidoHttpClient } from '@autofleet/rapido-http-client';
|
|
740
|
+
|
|
741
|
+
const client = new RapidoHttpClient({
|
|
742
|
+
serviceUrl: 'https://api.example.com',
|
|
743
|
+
logger,
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
const { data } = await client.get('/data');
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### With Retries
|
|
750
|
+
|
|
751
|
+
**Before (Network):**
|
|
752
|
+
```typescript
|
|
753
|
+
const network = new Network({
|
|
754
|
+
serviceUrl: 'https://api.example.com',
|
|
755
|
+
logger,
|
|
756
|
+
retries: 3,
|
|
757
|
+
retryDelay: (retryCount) => retryCount * 1000,
|
|
758
|
+
});
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
**After (Rapido HTTP Client):**
|
|
762
|
+
```typescript
|
|
763
|
+
const client = new RapidoHttpClient({
|
|
764
|
+
serviceUrl: 'https://api.example.com',
|
|
765
|
+
logger,
|
|
766
|
+
retries: {
|
|
767
|
+
maxRetries: 3,
|
|
768
|
+
minTimeout: 1000,
|
|
769
|
+
timeoutFactor: 1,
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
### Key Differences
|
|
775
|
+
|
|
776
|
+
1. **Default Type**: Rapido HTTP Client uses `unknown` instead of `any` for better type safety
|
|
777
|
+
2. **Error Handling**: Uses undici's error classes (`ResponseError`, `RequestAbortedError`, `UndiciError`)
|
|
778
|
+
3. **Cache**: Built-in HTTP caching support (no external plugin needed)
|
|
779
|
+
4. **Schema Validation**: Native Zod support with type inference
|
|
780
|
+
5. **Disposal**: Supports async disposal pattern (`await using`)
|