or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

connection-management.mdcookies.mderror-handling.mdhttp-api.mdhttp-clients.mdindex.mdinterceptors.mdtesting-mocking.mdutilities.mdweb-standards.md

interceptors.mddocs/

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

```