0
# Interceptors and Middleware
1
2
Composable interceptor system for request/response processing with built-in interceptors for common needs like retries, caching, and compression.
3
4
## Capabilities
5
6
### Interceptor System
7
8
The interceptor system allows composing middleware-style request/response processing through dispatcher composition.
9
10
```javascript { .api }
11
/**
12
* Built-in interceptors for common HTTP client needs
13
*/
14
const interceptors: {
15
redirect(options?: RedirectInterceptorOpts): DispatcherComposeInterceptor;
16
retry(options?: RetryInterceptorOpts): DispatcherComposeInterceptor;
17
cache(options?: CacheInterceptorOpts): DispatcherComposeInterceptor;
18
decompress(options?: DecompressInterceptorOpts): DispatcherComposeInterceptor;
19
dump(options?: DumpInterceptorOpts): DispatcherComposeInterceptor;
20
dns(options?: DnsInterceptorOpts): DispatcherComposeInterceptor;
21
responseError(options?: ResponseErrorInterceptorOpts): DispatcherComposeInterceptor;
22
};
23
24
type DispatcherComposeInterceptor = (dispatch: Dispatcher['dispatch']) => Dispatcher['dispatch'];
25
26
interface Dispatcher {
27
compose(interceptors: DispatcherComposeInterceptor[]): ComposedDispatcher;
28
compose(...interceptors: DispatcherComposeInterceptor[]): ComposedDispatcher;
29
}
30
```
31
32
**Usage Examples:**
33
34
```javascript
35
import { Agent, interceptors } from 'undici';
36
37
// Create dispatcher with multiple interceptors
38
const agent = new Agent()
39
.compose(
40
interceptors.retry({ maxRetries: 3 }),
41
interceptors.redirect({ maxRedirections: 5 }),
42
interceptors.decompress()
43
);
44
45
// Use composed dispatcher
46
const response = await agent.request({
47
origin: 'https://api.example.com',
48
path: '/data'
49
});
50
```
51
52
### Redirect Interceptor
53
54
Automatic HTTP redirect handling with configurable limits and policies.
55
56
```javascript { .api }
57
/**
58
* HTTP redirect interceptor
59
* @param options - Redirect configuration
60
* @returns Interceptor function
61
*/
62
function redirect(options?: RedirectInterceptorOpts): DispatcherComposeInterceptor;
63
64
interface RedirectInterceptorOpts {
65
maxRedirections?: number;
66
throwOnMaxRedirections?: boolean;
67
}
68
```
69
70
**Usage Examples:**
71
72
```javascript
73
import { Pool, interceptors } from 'undici';
74
75
// Configure redirect behavior
76
const pool = new Pool('https://api.example.com')
77
.compose(interceptors.redirect({
78
maxRedirections: 10,
79
throwOnMaxRedirections: true
80
}));
81
82
// Requests automatically follow redirects
83
const response = await pool.request({
84
path: '/redirect-chain',
85
method: 'GET'
86
});
87
88
console.log(response.context.history); // Array of redirected URLs
89
```
90
91
### Retry Interceptor
92
93
Automatic request retry with exponential backoff and configurable retry conditions.
94
95
```javascript { .api }
96
/**
97
* Request retry interceptor
98
* @param options - Retry configuration
99
* @returns Interceptor function
100
*/
101
function retry(options?: RetryInterceptorOpts): DispatcherComposeInterceptor;
102
103
interface RetryInterceptorOpts {
104
retry?: (err: Error, context: RetryContext) => number | null;
105
maxRetries?: number;
106
maxTimeout?: number;
107
minTimeout?: number;
108
timeoutFactor?: number;
109
retryAfter?: boolean;
110
methods?: string[];
111
statusCodes?: number[];
112
errorCodes?: string[];
113
}
114
115
interface RetryContext {
116
state: RetryState;
117
opts: RetryInterceptorOpts;
118
}
119
120
interface RetryState {
121
counter: number;
122
currentTimeout: number;
123
}
124
```
125
126
**Usage Examples:**
127
128
```javascript
129
import { Agent, interceptors } from 'undici';
130
131
// Basic retry configuration
132
const agent = new Agent()
133
.compose(interceptors.retry({
134
maxRetries: 3,
135
methods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'],
136
statusCodes: [408, 413, 429, 500, 502, 503, 504],
137
errorCodes: ['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN']
138
}));
139
140
// Custom retry logic with exponential backoff
141
const customRetryAgent = new Agent()
142
.compose(interceptors.retry({
143
retry: (err, { state, opts }) => {
144
const { counter, currentTimeout } = state;
145
146
if (counter >= opts.maxRetries) {
147
return null; // Stop retrying
148
}
149
150
// Exponential backoff with jitter
151
const delay = Math.min(
152
currentTimeout * Math.pow(2, counter),
153
opts.maxTimeout || 30000
154
);
155
156
return delay + Math.random() * 1000;
157
},
158
maxRetries: 5,
159
maxTimeout: 30000,
160
minTimeout: 1000,
161
retryAfter: true // Respect Retry-After header
162
}));
163
164
// Make request with retry
165
const response = await customRetryAgent.request({
166
origin: 'https://unreliable-api.example.com',
167
path: '/data'
168
});
169
```
170
171
### Cache Interceptor
172
173
HTTP caching interceptor with configurable cache stores and policies.
174
175
```javascript { .api }
176
/**
177
* HTTP caching interceptor
178
* @param options - Cache configuration
179
* @returns Interceptor function
180
*/
181
function cache(options?: CacheInterceptorOpts): DispatcherComposeInterceptor;
182
183
interface CacheInterceptorOpts {
184
store?: CacheStore;
185
methods?: string[];
186
vary?: string[];
187
cacheByDefault?: number;
188
type?: 'shared' | 'private';
189
}
190
191
interface CacheStore {
192
get(key: string): Promise<CacheValue | undefined>;
193
set(key: string, value: CacheValue, ttl?: number): Promise<void>;
194
delete(key: string): Promise<boolean>;
195
}
196
197
interface CacheValue {
198
statusCode: number;
199
statusMessage: string;
200
headers: Record<string, string>;
201
body: Buffer;
202
cacheControlDirectives: Record<string, string | boolean>;
203
vary: Record<string, string>;
204
}
205
```
206
207
**Usage Examples:**
208
209
```javascript
210
import { Agent, interceptors, cacheStores } from 'undici';
211
212
// Use memory cache store
213
const agent = new Agent()
214
.compose(interceptors.cache({
215
store: new cacheStores.MemoryCacheStore(),
216
methods: ['GET', 'HEAD'],
217
cacheByDefault: 300, // 5 minutes default TTL
218
type: 'private'
219
}));
220
221
// Use SQLite cache store for persistence
222
const persistentAgent = new Agent()
223
.compose(interceptors.cache({
224
store: new cacheStores.SqliteCacheStore('./http-cache.db'),
225
methods: ['GET'],
226
vary: ['user-agent', 'accept-encoding']
227
}));
228
229
// Make cacheable requests
230
const response1 = await agent.request({
231
origin: 'https://api.example.com',
232
path: '/users'
233
});
234
235
// Second request hits cache
236
const response2 = await agent.request({
237
origin: 'https://api.example.com',
238
path: '/users'
239
});
240
```
241
242
### Decompress Interceptor
243
244
Automatic response decompression for gzip, deflate, and brotli encodings.
245
246
```javascript { .api }
247
/**
248
* Response decompression interceptor
249
* @param options - Decompression configuration
250
* @returns Interceptor function
251
*/
252
function decompress(options?: DecompressInterceptorOpts): DispatcherComposeInterceptor;
253
254
interface DecompressInterceptorOpts {
255
encodings?: string[];
256
maxResponseSize?: number;
257
}
258
```
259
260
**Usage Examples:**
261
262
```javascript
263
import { Pool, interceptors } from 'undici';
264
265
// Enable automatic decompression
266
const pool = new Pool('https://api.example.com')
267
.compose(interceptors.decompress({
268
encodings: ['gzip', 'deflate', 'br'], // brotli, gzip, deflate
269
maxResponseSize: 10 * 1024 * 1024 // 10MB limit
270
}));
271
272
// Requests automatically include Accept-Encoding header
273
// and responses are decompressed transparently
274
const response = await pool.request({
275
path: '/compressed-data',
276
method: 'GET'
277
});
278
279
const data = await response.body.json();
280
```
281
282
### Dump Interceptor
283
284
Request/response dumping for debugging and logging purposes.
285
286
```javascript { .api }
287
/**
288
* Request/response dumping interceptor
289
* @param options - Dump configuration
290
* @returns Interceptor function
291
*/
292
function dump(options?: DumpInterceptorOpts): DispatcherComposeInterceptor;
293
294
interface DumpInterceptorOpts {
295
request?: boolean;
296
response?: boolean;
297
requestHeaders?: boolean;
298
responseHeaders?: boolean;
299
requestBody?: boolean;
300
responseBody?: boolean;
301
maxBodySize?: number;
302
logger?: (message: string) => void;
303
}
304
```
305
306
**Usage Examples:**
307
308
```javascript
309
import { Client, interceptors } from 'undici';
310
311
// Dump all request/response data
312
const client = new Client('https://api.example.com')
313
.compose(interceptors.dump({
314
request: true,
315
response: true,
316
requestHeaders: true,
317
responseHeaders: true,
318
requestBody: true,
319
responseBody: true,
320
maxBodySize: 1024, // Only dump first 1KB of body
321
logger: console.log
322
}));
323
324
// All requests will be logged
325
const response = await client.request({
326
path: '/debug',
327
method: 'POST',
328
body: JSON.stringify({ test: 'data' })
329
});
330
331
// Custom logger
332
const debugClient = new Client('https://api.example.com')
333
.compose(interceptors.dump({
334
response: true,
335
responseHeaders: true,
336
logger: (message) => {
337
// Custom logging logic
338
console.log(`[HTTP DEBUG] ${new Date().toISOString()} ${message}`);
339
}
340
}));
341
```
342
343
### DNS Interceptor
344
345
DNS caching and resolution interceptor for improved performance.
346
347
```javascript { .api }
348
/**
349
* DNS caching and resolution interceptor
350
* @param options - DNS configuration
351
* @returns Interceptor function
352
*/
353
function dns(options?: DnsInterceptorOpts): DispatcherComposeInterceptor;
354
355
interface DnsInterceptorOpts {
356
maxItems?: number;
357
maxTtl?: number;
358
lookup?: (hostname: string, options: any, callback: (err: Error | null, address: string, family: number) => void) => void;
359
}
360
```
361
362
**Usage Examples:**
363
364
```javascript
365
import { Agent, interceptors } from 'undici';
366
import { lookup } from 'dns';
367
368
// DNS caching for improved performance
369
const agent = new Agent()
370
.compose(interceptors.dns({
371
maxItems: 100, // Cache up to 100 DNS entries
372
maxTtl: 300000, // 5 minutes TTL
373
lookup: lookup // Use Node.js built-in DNS lookup
374
}));
375
376
// Subsequent requests to same hostname use cached DNS
377
const responses = await Promise.all([
378
agent.request({ origin: 'https://api.example.com', path: '/endpoint1' }),
379
agent.request({ origin: 'https://api.example.com', path: '/endpoint2' }),
380
agent.request({ origin: 'https://api.example.com', path: '/endpoint3' })
381
]);
382
```
383
384
### Response Error Interceptor
385
386
Enhanced error handling for HTTP response errors with detailed error information.
387
388
```javascript { .api }
389
/**
390
* Response error handling interceptor
391
* @param options - Error handling configuration
392
* @returns Interceptor function
393
*/
394
function responseError(options?: ResponseErrorInterceptorOpts): DispatcherComposeInterceptor;
395
396
interface ResponseErrorInterceptorOpts {
397
throwOnError?: boolean;
398
statusCodes?: number[];
399
includeResponseBody?: boolean;
400
maxResponseBodySize?: number;
401
}
402
```
403
404
**Usage Examples:**
405
406
```javascript
407
import { Pool, interceptors } from 'undici';
408
409
// Throw errors for 4xx and 5xx responses
410
const pool = new Pool('https://api.example.com')
411
.compose(interceptors.responseError({
412
throwOnError: true,
413
statusCodes: [400, 401, 403, 404, 500, 502, 503, 504],
414
includeResponseBody: true,
415
maxResponseBodySize: 1024
416
}));
417
418
try {
419
const response = await pool.request({
420
path: '/nonexistent',
421
method: 'GET'
422
});
423
} catch (error) {
424
console.log(error.statusCode); // 404
425
console.log(error.statusMessage); // Not Found
426
console.log(error.responseBody); // Error response body
427
}
428
```
429
430
## Custom Interceptors
431
432
Create custom interceptors for application-specific needs.
433
434
```javascript { .api }
435
/**
436
* Custom interceptor example
437
*/
438
function customInterceptor(options = {}) {
439
return (dispatch) => {
440
return (opts, handler) => {
441
// Pre-request processing
442
const modifiedOpts = {
443
...opts,
444
headers: {
445
...opts.headers,
446
'x-custom-header': 'custom-value'
447
}
448
};
449
450
// Wrap handler for post-response processing
451
const wrappedHandler = {
452
...handler,
453
onComplete(trailers) {
454
// Post-response processing
455
console.log('Request completed');
456
handler.onComplete(trailers);
457
},
458
onError(error) {
459
// Error processing
460
console.log('Request failed:', error.message);
461
handler.onError(error);
462
}
463
};
464
465
return dispatch(modifiedOpts, wrappedHandler);
466
};
467
};
468
}
469
```
470
471
**Usage Examples:**
472
473
```javascript
474
import { Agent } from 'undici';
475
476
// Authentication interceptor
477
function authInterceptor(token) {
478
return (dispatch) => {
479
return (opts, handler) => {
480
return dispatch({
481
...opts,
482
headers: {
483
...opts.headers,
484
'authorization': `Bearer ${token}`
485
}
486
}, handler);
487
};
488
};
489
}
490
491
// Rate limiting interceptor
492
function rateLimitInterceptor(requestsPerSecond = 10) {
493
let lastRequestTime = 0;
494
const minInterval = 1000 / requestsPerSecond;
495
496
return (dispatch) => {
497
return async (opts, handler) => {
498
const now = Date.now();
499
const timeSinceLastRequest = now - lastRequestTime;
500
501
if (timeSinceLastRequest < minInterval) {
502
await new Promise(resolve =>
503
setTimeout(resolve, minInterval - timeSinceLastRequest)
504
);
505
}
506
507
lastRequestTime = Date.now();
508
return dispatch(opts, handler);
509
};
510
};
511
}
512
513
// Use custom interceptors
514
const agent = new Agent()
515
.compose(
516
authInterceptor('your-auth-token'),
517
rateLimitInterceptor(5), // 5 requests per second
518
customInterceptor({ option: 'value' })
519
);
520
```
521
522
## Complete Interceptor Chain Example
523
524
```javascript
525
import { Agent, interceptors, cacheStores } from 'undici';
526
527
// Create comprehensive HTTP client with all features
528
const httpClient = new Agent({
529
factory: (origin, opts) => {
530
return new Pool(origin, {
531
...opts,
532
connections: 10,
533
pipelining: 1
534
});
535
}
536
})
537
.compose(
538
// Request/response logging
539
interceptors.dump({
540
request: true,
541
response: true,
542
requestHeaders: true,
543
responseHeaders: true,
544
logger: (msg) => console.log(`[HTTP] ${msg}`)
545
}),
546
547
// DNS caching
548
interceptors.dns({
549
maxItems: 100,
550
maxTtl: 300000
551
}),
552
553
// HTTP caching
554
interceptors.cache({
555
store: new cacheStores.MemoryCacheStore(),
556
methods: ['GET', 'HEAD'],
557
cacheByDefault: 300
558
}),
559
560
// Automatic decompression
561
interceptors.decompress(),
562
563
// Automatic redirects
564
interceptors.redirect({
565
maxRedirections: 5
566
}),
567
568
// Retry with exponential backoff
569
interceptors.retry({
570
maxRetries: 3,
571
methods: ['GET', 'HEAD', 'OPTIONS'],
572
statusCodes: [408, 413, 429, 500, 502, 503, 504]
573
}),
574
575
// Enhanced error handling
576
interceptors.responseError({
577
throwOnError: true,
578
statusCodes: [400, 401, 403, 404, 500, 502, 503],
579
includeResponseBody: true
580
})
581
);
582
583
// All features work together automatically
584
const response = await httpClient.request({
585
origin: 'https://api.example.com',
586
path: '/data',
587
method: 'GET'
588
});
589
```