0
# Interceptors
1
2
Request/response transformation and error handling middleware system. Interceptors provide a powerful way to modify requests and responses, implement retry logic, handle redirects, and add cross-cutting concerns to HTTP operations.
3
4
## Capabilities
5
6
### Core Interceptor Interface
7
8
```typescript { .api }
9
/**
10
* Base interceptor interface for request/response transformation
11
*/
12
interface Interceptor {
13
(dispatch: Dispatcher['dispatch']): Dispatcher['dispatch'];
14
}
15
16
/**
17
* Interceptor options base interface
18
*/
19
interface InterceptorOptions {
20
/** Maximum number of redirects/retries */
21
maxRedirections?: number;
22
}
23
```
24
25
### Dump Interceptor
26
27
Captures and logs request/response data for debugging and monitoring.
28
29
```typescript { .api }
30
/**
31
* Creates interceptor that captures request/response data
32
* @param options - Dump configuration options
33
* @returns Interceptor function
34
*/
35
function dump(options?: DumpInterceptorOpts): Interceptor;
36
37
interface DumpInterceptorOpts {
38
/** Maximum body size to capture (bytes) */
39
maxSize?: number;
40
41
/** Whether to capture request body */
42
captureRequestBody?: boolean;
43
44
/** Whether to capture response body */
45
captureResponseBody?: boolean;
46
47
/** Custom logging function */
48
logger?: (data: DumpData) => void;
49
}
50
51
interface DumpData {
52
request: {
53
origin: string;
54
method: string;
55
path: string;
56
headers: Record<string, string | string[]>;
57
body?: string | Buffer;
58
};
59
response: {
60
statusCode: number;
61
headers: Record<string, string | string[]>;
62
body?: string | Buffer;
63
};
64
timestamp: number;
65
duration: number;
66
}
67
```
68
69
**Usage Examples:**
70
71
```typescript
72
import { Client, interceptors } from "undici-types";
73
74
// Basic dump interceptor
75
const client = new Client("https://api.example.com")
76
.compose(interceptors.dump());
77
78
// Request will be logged to console
79
const response = await client.request({
80
path: "/users",
81
method: "GET"
82
});
83
84
// Dump with custom options
85
const dumpClient = new Client("https://api.example.com")
86
.compose(interceptors.dump({
87
maxSize: 10240, // 10KB max
88
captureRequestBody: true,
89
captureResponseBody: true,
90
logger: (data) => {
91
console.log(`${data.request.method} ${data.request.path} - ${data.response.statusCode} (${data.duration}ms)`);
92
console.log("Request headers:", data.request.headers);
93
console.log("Response headers:", data.response.headers);
94
95
if (data.request.body) {
96
console.log("Request body:", data.request.body.toString());
97
}
98
99
if (data.response.body) {
100
console.log("Response body:", data.response.body.toString());
101
}
102
}
103
}));
104
105
// Make requests with detailed logging
106
await dumpClient.request({
107
path: "/users",
108
method: "POST",
109
body: JSON.stringify({ name: "John Doe" }),
110
headers: { "content-type": "application/json" }
111
});
112
```
113
114
### Retry Interceptor
115
116
Automatic retry logic for failed requests with configurable strategies.
117
118
```typescript { .api }
119
/**
120
* Creates interceptor that retries failed requests
121
* @param options - Retry configuration options
122
* @returns Interceptor function
123
*/
124
function retry(options?: RetryInterceptorOpts): Interceptor;
125
126
interface RetryInterceptorOpts extends InterceptorOptions {
127
/** Number of retry attempts */
128
retry?: number;
129
130
/** HTTP methods to retry */
131
methods?: HttpMethod[];
132
133
/** HTTP status codes to retry */
134
statusCodes?: number[];
135
136
/** Error codes to retry */
137
errorCodes?: string[];
138
139
/** Minimum delay between retries (ms) */
140
minTimeout?: number;
141
142
/** Maximum delay between retries (ms) */
143
maxTimeout?: number;
144
145
/** Multiplier for exponential backoff */
146
timeoutFactor?: number;
147
148
/** Maximum delay from Retry-After header (ms) */
149
maxRetryAfter?: number;
150
151
/** Whether to respect Retry-After headers */
152
retryAfter?: boolean;
153
154
/** Custom retry condition function */
155
retryCondition?: (error: Error, context: RetryContext) => boolean;
156
}
157
158
interface RetryContext {
159
attempt: number;
160
maxAttempts: number;
161
error: Error;
162
request: {
163
method: string;
164
path: string;
165
headers: Record<string, string | string[]>;
166
};
167
}
168
169
type HttpMethod = "GET" | "HEAD" | "OPTIONS" | "PUT" | "DELETE" | "TRACE";
170
```
171
172
**Usage Examples:**
173
174
```typescript
175
import { Client, interceptors } from "undici-types";
176
177
// Basic retry interceptor
178
const retryClient = new Client("https://unreliable-api.example.com")
179
.compose(interceptors.retry({
180
retry: 3,
181
methods: ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"],
182
statusCodes: [408, 413, 429, 500, 502, 503, 504],
183
errorCodes: ["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ENETDOWN"]
184
}));
185
186
// Advanced retry with custom backoff
187
const advancedRetryClient = new Client("https://api.example.com")
188
.compose(interceptors.retry({
189
retry: 5,
190
minTimeout: 1000,
191
maxTimeout: 30000,
192
timeoutFactor: 2,
193
retryAfter: true, // Respect Retry-After headers
194
maxRetryAfter: 60000,
195
retryCondition: (error, context) => {
196
// Custom retry logic
197
if (context.attempt >= 3 && error.message.includes("rate limit")) {
198
return false; // Don't retry rate limits after 3 attempts
199
}
200
201
// Default retry for network errors
202
return context.attempt < context.maxAttempts;
203
}
204
}));
205
206
// Requests automatically retry on failure
207
try {
208
const response = await retryClient.request({
209
path: "/flaky-endpoint",
210
method: "GET"
211
});
212
} catch (error) {
213
// Error thrown only after all retry attempts failed
214
console.error("Request failed after retries:", error);
215
}
216
```
217
218
### Redirect Interceptor
219
220
Automatic handling of HTTP redirects with security controls.
221
222
```typescript { .api }
223
/**
224
* Creates interceptor that follows HTTP redirects
225
* @param options - Redirect configuration options
226
* @returns Interceptor function
227
*/
228
function redirect(options?: RedirectInterceptorOpts): Interceptor;
229
230
interface RedirectInterceptorOpts extends InterceptorOptions {
231
/** Maximum number of redirects to follow */
232
maxRedirections?: number;
233
234
/** Whether to preserve request body on redirects */
235
throwOnMaxRedirect?: boolean;
236
237
/** Custom redirect validation function */
238
beforeRedirect?: (options: {
239
headers: Record<string, string | string[]>;
240
statusCode: number;
241
location: string;
242
opaque: unknown;
243
}) => void;
244
245
/** HTTP methods allowed for redirects */
246
allowedMethods?: HttpMethod[];
247
248
/** Whether to follow redirects to different origins */
249
allowCrossOrigin?: boolean;
250
}
251
```
252
253
**Usage Examples:**
254
255
```typescript
256
import { Client, interceptors } from "undici-types";
257
258
// Basic redirect interceptor
259
const redirectClient = new Client("https://api.example.com")
260
.compose(interceptors.redirect({
261
maxRedirections: 10
262
}));
263
264
// Secure redirect handling
265
const secureRedirectClient = new Client("https://api.example.com")
266
.compose(interceptors.redirect({
267
maxRedirections: 5,
268
allowCrossOrigin: false, // Don't follow cross-origin redirects
269
allowedMethods: ["GET", "HEAD"], // Only follow redirects for safe methods
270
beforeRedirect: ({ headers, statusCode, location }) => {
271
console.log(`Redirecting ${statusCode} to ${location}`);
272
273
// Validate redirect destination
274
const url = new URL(location);
275
if (!url.hostname.endsWith('.trusted-domain.com')) {
276
throw new Error('Redirect to untrusted domain blocked');
277
}
278
},
279
throwOnMaxRedirect: true
280
}));
281
282
// Custom redirect logging
283
const loggingRedirectClient = new Client("https://api.example.com")
284
.compose(interceptors.redirect({
285
maxRedirections: 3,
286
beforeRedirect: ({ statusCode, location, headers }) => {
287
console.log(`Following ${statusCode} redirect to: ${location}`);
288
289
// Log redirect chain for debugging
290
const cacheControl = headers['cache-control'];
291
if (cacheControl) {
292
console.log(`Cache-Control: ${cacheControl}`);
293
}
294
}
295
}));
296
297
// Request follows redirects automatically
298
const response = await redirectClient.request({
299
path: "/redirect-me",
300
method: "GET"
301
});
302
303
console.log(`Final URL: ${response.context.history}`); // Redirect history
304
```
305
306
### Decompression Interceptor
307
308
Automatic decompression of compressed response bodies.
309
310
```typescript { .api }
311
/**
312
* Creates interceptor that decompresses response bodies
313
* @param options - Decompression configuration options
314
* @returns Interceptor function
315
*/
316
function decompress(options?: DecompressInterceptorOpts): Interceptor;
317
318
interface DecompressInterceptorOpts {
319
/** Compression formats to support */
320
supportedEncodings?: string[];
321
322
/** Maximum decompressed size (bytes) */
323
maxSize?: number;
324
325
/** Whether to throw on unsupported encoding */
326
throwOnUnsupportedEncoding?: boolean;
327
}
328
```
329
330
**Usage Examples:**
331
332
```typescript
333
import { Client, interceptors } from "undici-types";
334
335
// Basic decompression
336
const decompressClient = new Client("https://api.example.com")
337
.compose(interceptors.decompress());
338
339
// Custom decompression options
340
const customDecompressClient = new Client("https://api.example.com")
341
.compose(interceptors.decompress({
342
supportedEncodings: ["gzip", "deflate", "br"], // Brotli support
343
maxSize: 50 * 1024 * 1024, // 50MB max decompressed size
344
throwOnUnsupportedEncoding: false
345
}));
346
347
// Automatically handles compressed responses
348
const response = await decompressClient.request({
349
path: "/compressed-data",
350
method: "GET",
351
headers: {
352
"accept-encoding": "gzip, deflate, br"
353
}
354
});
355
356
// Response body is automatically decompressed
357
const data = await response.body.text();
358
```
359
360
### Response Error Interceptor
361
362
Automatic error throwing for HTTP error status codes.
363
364
```typescript { .api }
365
/**
366
* Creates interceptor that throws errors for HTTP error status codes
367
* @param options - Response error configuration options
368
* @returns Interceptor function
369
*/
370
function responseError(options?: ResponseErrorInterceptorOpts): Interceptor;
371
372
interface ResponseErrorInterceptorOpts {
373
/** Status codes that should throw errors */
374
statusCodes?: number[];
375
376
/** Whether to include response body in error */
377
includeBody?: boolean;
378
379
/** Custom error factory function */
380
errorFactory?: (response: {
381
statusCode: number;
382
headers: Record<string, string | string[]>;
383
body: any;
384
}) => Error;
385
}
386
```
387
388
**Usage Examples:**
389
390
```typescript
391
import { Client, interceptors, ResponseStatusCodeError } from "undici-types";
392
393
// Basic error throwing for 4xx/5xx status codes
394
const errorClient = new Client("https://api.example.com")
395
.compose(interceptors.responseError({
396
statusCodes: [400, 401, 403, 404, 500, 502, 503, 504]
397
}));
398
399
// Custom error handling
400
const customErrorClient = new Client("https://api.example.com")
401
.compose(interceptors.responseError({
402
includeBody: true,
403
errorFactory: ({ statusCode, headers, body }) => {
404
if (statusCode === 401) {
405
return new Error(`Authentication failed: ${body.message}`);
406
}
407
408
if (statusCode === 429) {
409
const retryAfter = headers['retry-after'];
410
return new Error(`Rate limited. Retry after: ${retryAfter}s`);
411
}
412
413
return new ResponseStatusCodeError(
414
`HTTP ${statusCode}`,
415
statusCode,
416
headers,
417
body
418
);
419
}
420
}));
421
422
// Requests throw errors for non-success status codes
423
try {
424
const response = await errorClient.request({
425
path: "/protected-resource",
426
method: "GET"
427
});
428
} catch (error) {
429
if (error instanceof ResponseStatusCodeError) {
430
console.error(`HTTP Error: ${error.status} ${error.statusText}`);
431
console.error("Response body:", error.body);
432
}
433
}
434
```
435
436
### DNS Interceptor
437
438
Custom DNS resolution for requests.
439
440
```typescript { .api }
441
/**
442
* Creates interceptor that customizes DNS resolution
443
* @param options - DNS configuration options
444
* @returns Interceptor function
445
*/
446
function dns(options: DNSInterceptorOpts): Interceptor;
447
448
interface DNSInterceptorOpts {
449
/** Maximum TTL for cached DNS entries */
450
maxTTL?: number;
451
452
/** Maximum number of cached items */
453
maxItems?: number;
454
455
/** Custom DNS lookup function */
456
lookup?: (
457
hostname: string,
458
options: LookupOptions,
459
callback: (err: NodeJS.ErrnoException | null, addresses: DNSInterceptorRecord[]) => void
460
) => void;
461
462
/** Custom pick function for selecting from multiple records */
463
pick?: (origin: URL, records: DNSInterceptorOriginRecords, affinity: 4 | 6) => DNSInterceptorRecord;
464
465
/** Enable dual stack (IPv4 and IPv6) */
466
dualStack?: boolean;
467
468
/** IP version affinity */
469
affinity?: 4 | 6;
470
}
471
472
interface DNSInterceptorOriginRecords {
473
4: { ips: DNSInterceptorRecord[] } | null;
474
6: { ips: DNSInterceptorRecord[] } | null;
475
}
476
477
interface DNSInterceptorRecord {
478
address: string;
479
ttl: number;
480
family: 4 | 6;
481
}
482
483
interface LookupOptions {
484
family?: 4 | 6 | 0;
485
hints?: number;
486
all?: boolean;
487
}
488
```
489
490
**Usage Examples:**
491
492
```typescript
493
import { Client, interceptors } from "undici-types";
494
495
// DNS interceptor with caching
496
const dnsClient = new Client("https://api.example.com")
497
.compose(interceptors.dns({
498
maxTTL: 300000, // 5 minute max TTL
499
maxItems: 100, // Cache up to 100 entries
500
dualStack: true,
501
affinity: 4 // Prefer IPv4
502
}));
503
504
// Custom DNS lookup
505
const customDnsClient = new Client("https://api.example.com")
506
.compose(interceptors.dns({
507
lookup: (hostname, options, callback) => {
508
// Custom DNS resolution logic
509
if (hostname === "api.example.com") {
510
callback(null, [{
511
address: "192.168.1.100",
512
ttl: 300,
513
family: 4
514
}]);
515
} else {
516
// Fallback to system DNS
517
require("dns").lookup(hostname, options, (err, address, family) => {
518
if (err) return callback(err, []);
519
callback(null, [{
520
address,
521
ttl: 300,
522
family: family as 4 | 6
523
}]);
524
});
525
}
526
}
527
}));
528
529
// Requests use custom DNS resolution
530
const response = await dnsClient.request({
531
path: "/data",
532
method: "GET"
533
});
534
```
535
536
### Cache Interceptor
537
538
HTTP response caching with RFC 7234 compliance.
539
540
```typescript { .api }
541
/**
542
* Creates interceptor that caches HTTP responses
543
* @param options - Cache configuration options
544
* @returns Interceptor function
545
*/
546
function cache(options?: CacheInterceptorOpts): Interceptor;
547
548
interface CacheInterceptorOpts {
549
/** Cache store implementation */
550
store?: CacheStore;
551
552
/** Cache methods */
553
methods?: string[];
554
555
/** Maximum cache age in milliseconds */
556
maxAge?: number;
557
558
/** Whether to cache responses with no explicit cache headers */
559
cacheDefault?: boolean;
560
561
/** Custom cache key generation */
562
generateCacheKey?: (request: {
563
origin: string;
564
method: string;
565
path: string;
566
headers: Record<string, string | string[]>;
567
}) => string;
568
}
569
570
interface CacheStore {
571
get(key: string): Promise<CacheValue | null>;
572
set(key: string, value: CacheValue, ttl?: number): Promise<void>;
573
delete(key: string): Promise<boolean>;
574
clear(): Promise<void>;
575
}
576
577
interface CacheValue {
578
statusCode: number;
579
headers: Record<string, string | string[]>;
580
body: Buffer;
581
cachedAt: number;
582
}
583
584
class MemoryCacheStore implements CacheStore {
585
constructor(opts?: MemoryCacheStoreOpts);
586
get(key: string): Promise<CacheValue | null>;
587
set(key: string, value: CacheValue, ttl?: number): Promise<void>;
588
delete(key: string): Promise<boolean>;
589
clear(): Promise<void>;
590
}
591
592
interface MemoryCacheStoreOpts {
593
maxItems?: number;
594
maxEntrySize?: number;
595
}
596
597
class SqliteCacheStore implements CacheStore {
598
constructor(opts?: SqliteCacheStoreOpts);
599
get(key: string): Promise<CacheValue | null>;
600
set(key: string, value: CacheValue, ttl?: number): Promise<void>;
601
delete(key: string): Promise<boolean>;
602
clear(): Promise<void>;
603
}
604
605
interface SqliteCacheStoreOpts {
606
location?: string;
607
maxCount?: number;
608
maxSize?: number;
609
maxEntrySize?: number;
610
}
611
```
612
613
**Usage Examples:**
614
615
```typescript
616
import { Client, interceptors, MemoryCacheStore, SqliteCacheStore } from "undici-types";
617
618
// Basic in-memory caching
619
const cacheClient = new Client("https://api.example.com")
620
.compose(interceptors.cache({
621
store: new MemoryCacheStore({
622
maxItems: 1000,
623
maxEntrySize: 1024 * 1024 // 1MB max per entry
624
}),
625
methods: ["GET", "HEAD"],
626
cacheByDefault: 300 // Cache for 5 minutes by default
627
}));
628
629
// SQLite-based persistent caching
630
const persistentCacheClient = new Client("https://api.example.com")
631
.compose(interceptors.cache({
632
store: new SqliteCacheStore({
633
location: "./cache.db",
634
maxCount: 10000,
635
maxSize: 100 * 1024 * 1024, // 100MB total cache size
636
maxEntrySize: 5 * 1024 * 1024 // 5MB max per entry
637
}),
638
methods: ["GET", "HEAD", "OPTIONS"]
639
}));
640
641
// Custom cache key generation
642
const customCacheClient = new Client("https://api.example.com")
643
.compose(interceptors.cache({
644
store: new MemoryCacheStore(),
645
generateCacheKey: ({ origin, method, path, headers }) => {
646
const userId = headers["x-user-id"];
647
return `${method}:${origin}${path}:${userId}`;
648
}
649
}));
650
651
// First request fetches from server
652
const response1 = await cacheClient.request({
653
path: "/data",
654
method: "GET"
655
});
656
657
// Second request served from cache
658
const response2 = await cacheClient.request({
659
path: "/data",
660
method: "GET"
661
});
662
```
663
664
## Interceptor Composition
665
666
Combining multiple interceptors for comprehensive request/response handling.
667
668
**Usage Examples:**
669
670
```typescript
671
import { Client, interceptors } from "undici-types";
672
673
// Compose multiple interceptors
674
const enhancedClient = new Client("https://api.example.com")
675
.compose(interceptors.dns({
676
origins: {
677
"https://api.example.com": [{ address: "10.0.0.1", family: 4 }]
678
}
679
}))
680
.compose(interceptors.retry({
681
retry: 3,
682
methods: ["GET", "HEAD", "PUT", "DELETE"]
683
}))
684
.compose(interceptors.redirect({
685
maxRedirections: 5
686
}))
687
.compose(interceptors.decompress())
688
.compose(interceptors.responseError({
689
statusCodes: [400, 401, 403, 404, 500, 502, 503, 504]
690
}))
691
.compose(interceptors.cache({
692
store: new MemoryCacheStore(),
693
methods: ["GET", "HEAD"]
694
}))
695
.compose(interceptors.dump({
696
logger: (data) => console.log(`${data.request.method} ${data.request.path} - ${data.response.statusCode}`)
697
}));
698
699
// All interceptors applied in composition order
700
const response = await enhancedClient.request({
701
path: "/api/resource",
702
method: "GET"
703
});
704
705
// Request flow:
706
// 1. DNS resolution (custom IP)
707
// 2. Retry on failure
708
// 3. Follow redirects
709
// 4. Decompress response
710
// 5. Throw on error status
711
// 6. Cache successful response
712
// 7. Log request/response
713
```