CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-undici

An HTTP/1.1 client, written from scratch for Node.js

Pending
Overview
Eval results
Files

interceptors.mddocs/

Interceptors and Middleware

Composable interceptor system for request/response processing with built-in interceptors for common needs like retries, caching, and compression.

Capabilities

Interceptor System

The interceptor system allows composing middleware-style request/response processing through dispatcher composition.

/**
 * Built-in interceptors for common HTTP client needs
 */
const interceptors: {
  redirect(options?: RedirectInterceptorOpts): DispatcherComposeInterceptor;
  retry(options?: RetryInterceptorOpts): DispatcherComposeInterceptor;
  cache(options?: CacheInterceptorOpts): DispatcherComposeInterceptor;
  decompress(options?: DecompressInterceptorOpts): DispatcherComposeInterceptor;
  dump(options?: DumpInterceptorOpts): DispatcherComposeInterceptor;
  dns(options?: DnsInterceptorOpts): DispatcherComposeInterceptor;
  responseError(options?: ResponseErrorInterceptorOpts): DispatcherComposeInterceptor;
};

type DispatcherComposeInterceptor = (dispatch: Dispatcher['dispatch']) => Dispatcher['dispatch'];

interface Dispatcher {
  compose(interceptors: DispatcherComposeInterceptor[]): ComposedDispatcher;
  compose(...interceptors: DispatcherComposeInterceptor[]): ComposedDispatcher;
}

Usage Examples:

import { Agent, interceptors } from 'undici';

// Create dispatcher with multiple interceptors
const agent = new Agent()
  .compose(
    interceptors.retry({ maxRetries: 3 }),
    interceptors.redirect({ maxRedirections: 5 }),
    interceptors.decompress()
  );

// Use composed dispatcher
const response = await agent.request({
  origin: 'https://api.example.com',
  path: '/data'
});

Redirect Interceptor

Automatic HTTP redirect handling with configurable limits and policies.

/**
 * HTTP redirect interceptor
 * @param options - Redirect configuration
 * @returns Interceptor function
 */
function redirect(options?: RedirectInterceptorOpts): DispatcherComposeInterceptor;

interface RedirectInterceptorOpts {
  maxRedirections?: number;
  throwOnMaxRedirections?: boolean;
}

Usage Examples:

import { Pool, interceptors } from 'undici';

// Configure redirect behavior
const pool = new Pool('https://api.example.com')
  .compose(interceptors.redirect({
    maxRedirections: 10,
    throwOnMaxRedirections: true
  }));

// Requests automatically follow redirects
const response = await pool.request({
  path: '/redirect-chain',
  method: 'GET'
});

console.log(response.context.history); // Array of redirected URLs

Retry Interceptor

Automatic request retry with exponential backoff and configurable retry conditions.

/**
 * Request retry interceptor
 * @param options - Retry configuration
 * @returns Interceptor function
 */
function retry(options?: RetryInterceptorOpts): DispatcherComposeInterceptor;

interface RetryInterceptorOpts {
  retry?: (err: Error, context: RetryContext) => number | null;
  maxRetries?: number;
  maxTimeout?: number;
  minTimeout?: number;
  timeoutFactor?: number;
  retryAfter?: boolean;
  methods?: string[];
  statusCodes?: number[];
  errorCodes?: string[];
}

interface RetryContext {
  state: RetryState;
  opts: RetryInterceptorOpts;
}

interface RetryState {
  counter: number;
  currentTimeout: number;
}

Usage Examples:

import { Agent, interceptors } from 'undici';

// Basic retry configuration
const agent = new Agent()
  .compose(interceptors.retry({
    maxRetries: 3,
    methods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'],
    statusCodes: [408, 413, 429, 500, 502, 503, 504],
    errorCodes: ['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN']
  }));

// Custom retry logic with exponential backoff
const customRetryAgent = new Agent()
  .compose(interceptors.retry({
    retry: (err, { state, opts }) => {
      const { counter, currentTimeout } = state;
      
      if (counter >= opts.maxRetries) {
        return null; // Stop retrying
      }
      
      // Exponential backoff with jitter
      const delay = Math.min(
        currentTimeout * Math.pow(2, counter),
        opts.maxTimeout || 30000
      );
      
      return delay + Math.random() * 1000;
    },
    maxRetries: 5,
    maxTimeout: 30000,
    minTimeout: 1000,
    retryAfter: true // Respect Retry-After header
  }));

// Make request with retry
const response = await customRetryAgent.request({
  origin: 'https://unreliable-api.example.com',
  path: '/data'
});

Cache Interceptor

HTTP caching interceptor with configurable cache stores and policies.

/**
 * HTTP caching interceptor
 * @param options - Cache configuration
 * @returns Interceptor function
 */
function cache(options?: CacheInterceptorOpts): DispatcherComposeInterceptor;

interface CacheInterceptorOpts {
  store?: CacheStore;
  methods?: string[];
  vary?: string[];
  cacheByDefault?: number;
  type?: 'shared' | 'private';
}

interface CacheStore {
  get(key: string): Promise<CacheValue | undefined>;
  set(key: string, value: CacheValue, ttl?: number): Promise<void>;
  delete(key: string): Promise<boolean>;
}

interface CacheValue {
  statusCode: number;
  statusMessage: string;
  headers: Record<string, string>;
  body: Buffer;
  cacheControlDirectives: Record<string, string | boolean>;
  vary: Record<string, string>;
}

Usage Examples:

import { Agent, interceptors, cacheStores } from 'undici';

// Use memory cache store
const agent = new Agent()
  .compose(interceptors.cache({
    store: new cacheStores.MemoryCacheStore(),
    methods: ['GET', 'HEAD'],
    cacheByDefault: 300, // 5 minutes default TTL
    type: 'private'
  }));

// Use SQLite cache store for persistence
const persistentAgent = new Agent()
  .compose(interceptors.cache({
    store: new cacheStores.SqliteCacheStore('./http-cache.db'),
    methods: ['GET'],
    vary: ['user-agent', 'accept-encoding']
  }));

// Make cacheable requests
const response1 = await agent.request({
  origin: 'https://api.example.com',
  path: '/users'
});

// Second request hits cache
const response2 = await agent.request({
  origin: 'https://api.example.com',
  path: '/users'
});

Decompress Interceptor

Automatic response decompression for gzip, deflate, and brotli encodings.

/**
 * Response decompression interceptor
 * @param options - Decompression configuration
 * @returns Interceptor function
 */
function decompress(options?: DecompressInterceptorOpts): DispatcherComposeInterceptor;

interface DecompressInterceptorOpts {
  encodings?: string[];
  maxResponseSize?: number;
}

Usage Examples:

import { Pool, interceptors } from 'undici';

// Enable automatic decompression
const pool = new Pool('https://api.example.com')
  .compose(interceptors.decompress({
    encodings: ['gzip', 'deflate', 'br'], // brotli, gzip, deflate
    maxResponseSize: 10 * 1024 * 1024 // 10MB limit
  }));

// Requests automatically include Accept-Encoding header
// and responses are decompressed transparently
const response = await pool.request({
  path: '/compressed-data',
  method: 'GET'
});

const data = await response.body.json();

Dump Interceptor

Request/response dumping for debugging and logging purposes.

/**
 * Request/response dumping interceptor
 * @param options - Dump configuration
 * @returns Interceptor function
 */
function dump(options?: DumpInterceptorOpts): DispatcherComposeInterceptor;

interface DumpInterceptorOpts {
  request?: boolean;
  response?: boolean;
  requestHeaders?: boolean;
  responseHeaders?: boolean;
  requestBody?: boolean;
  responseBody?: boolean;
  maxBodySize?: number;
  logger?: (message: string) => void;
}

Usage Examples:

import { Client, interceptors } from 'undici';

// Dump all request/response data
const client = new Client('https://api.example.com')
  .compose(interceptors.dump({
    request: true,
    response: true,
    requestHeaders: true,
    responseHeaders: true,
    requestBody: true,
    responseBody: true,
    maxBodySize: 1024, // Only dump first 1KB of body
    logger: console.log
  }));

// All requests will be logged
const response = await client.request({
  path: '/debug',
  method: 'POST',
  body: JSON.stringify({ test: 'data' })
});

// Custom logger
const debugClient = new Client('https://api.example.com')
  .compose(interceptors.dump({
    response: true,
    responseHeaders: true,
    logger: (message) => {
      // Custom logging logic
      console.log(`[HTTP DEBUG] ${new Date().toISOString()} ${message}`);
    }
  }));

DNS Interceptor

DNS caching and resolution interceptor for improved performance.

/**
 * DNS caching and resolution interceptor
 * @param options - DNS configuration
 * @returns Interceptor function
 */
function dns(options?: DnsInterceptorOpts): DispatcherComposeInterceptor;

interface DnsInterceptorOpts {
  maxItems?: number;
  maxTtl?: number;
  lookup?: (hostname: string, options: any, callback: (err: Error | null, address: string, family: number) => void) => void;
}

Usage Examples:

import { Agent, interceptors } from 'undici';
import { lookup } from 'dns';

// DNS caching for improved performance
const agent = new Agent()
  .compose(interceptors.dns({
    maxItems: 100,     // Cache up to 100 DNS entries
    maxTtl: 300000,    // 5 minutes TTL
    lookup: lookup     // Use Node.js built-in DNS lookup
  }));

// Subsequent requests to same hostname use cached DNS
const responses = await Promise.all([
  agent.request({ origin: 'https://api.example.com', path: '/endpoint1' }),
  agent.request({ origin: 'https://api.example.com', path: '/endpoint2' }),
  agent.request({ origin: 'https://api.example.com', path: '/endpoint3' })
]);

Response Error Interceptor

Enhanced error handling for HTTP response errors with detailed error information.

/**
 * Response error handling interceptor
 * @param options - Error handling configuration
 * @returns Interceptor function
 */
function responseError(options?: ResponseErrorInterceptorOpts): DispatcherComposeInterceptor;

interface ResponseErrorInterceptorOpts {
  throwOnError?: boolean;
  statusCodes?: number[];
  includeResponseBody?: boolean;
  maxResponseBodySize?: number;
}

Usage Examples:

import { Pool, interceptors } from 'undici';

// Throw errors for 4xx and 5xx responses
const pool = new Pool('https://api.example.com')
  .compose(interceptors.responseError({
    throwOnError: true,
    statusCodes: [400, 401, 403, 404, 500, 502, 503, 504],
    includeResponseBody: true,
    maxResponseBodySize: 1024
  }));

try {
  const response = await pool.request({
    path: '/nonexistent',
    method: 'GET'
  });
} catch (error) {
  console.log(error.statusCode);    // 404
  console.log(error.statusMessage); // Not Found
  console.log(error.responseBody);  // Error response body
}

Custom Interceptors

Create custom interceptors for application-specific needs.

/**
 * Custom interceptor example
 */
function customInterceptor(options = {}) {
  return (dispatch) => {
    return (opts, handler) => {
      // Pre-request processing
      const modifiedOpts = {
        ...opts,
        headers: {
          ...opts.headers,
          'x-custom-header': 'custom-value'
        }
      };
      
      // Wrap handler for post-response processing
      const wrappedHandler = {
        ...handler,
        onComplete(trailers) {
          // Post-response processing
          console.log('Request completed');
          handler.onComplete(trailers);
        },
        onError(error) {
          // Error processing
          console.log('Request failed:', error.message);
          handler.onError(error);
        }
      };
      
      return dispatch(modifiedOpts, wrappedHandler);
    };
  };
}

Usage Examples:

import { Agent } from 'undici';

// Authentication interceptor
function authInterceptor(token) {
  return (dispatch) => {
    return (opts, handler) => {
      return dispatch({
        ...opts,
        headers: {
          ...opts.headers,
          'authorization': `Bearer ${token}`
        }
      }, handler);
    };
  };
}

// Rate limiting interceptor
function rateLimitInterceptor(requestsPerSecond = 10) {
  let lastRequestTime = 0;
  const minInterval = 1000 / requestsPerSecond;
  
  return (dispatch) => {
    return async (opts, handler) => {
      const now = Date.now();
      const timeSinceLastRequest = now - lastRequestTime;
      
      if (timeSinceLastRequest < minInterval) {
        await new Promise(resolve => 
          setTimeout(resolve, minInterval - timeSinceLastRequest)
        );
      }
      
      lastRequestTime = Date.now();
      return dispatch(opts, handler);
    };
  };
}

// Use custom interceptors
const agent = new Agent()
  .compose(
    authInterceptor('your-auth-token'),
    rateLimitInterceptor(5), // 5 requests per second
    customInterceptor({ option: 'value' })
  );

Complete Interceptor Chain Example

import { Agent, interceptors, cacheStores } from 'undici';

// Create comprehensive HTTP client with all features
const httpClient = new Agent({
  factory: (origin, opts) => {
    return new Pool(origin, { 
      ...opts, 
      connections: 10,
      pipelining: 1
    });
  }
})
.compose(
  // Request/response logging
  interceptors.dump({
    request: true,
    response: true,
    requestHeaders: true,
    responseHeaders: true,
    logger: (msg) => console.log(`[HTTP] ${msg}`)
  }),
  
  // DNS caching
  interceptors.dns({
    maxItems: 100,
    maxTtl: 300000
  }),
  
  // HTTP caching
  interceptors.cache({
    store: new cacheStores.MemoryCacheStore(),
    methods: ['GET', 'HEAD'],
    cacheByDefault: 300
  }),
  
  // Automatic decompression
  interceptors.decompress(),
  
  // Automatic redirects
  interceptors.redirect({
    maxRedirections: 5
  }),
  
  // Retry with exponential backoff
  interceptors.retry({
    maxRetries: 3,
    methods: ['GET', 'HEAD', 'OPTIONS'],
    statusCodes: [408, 413, 429, 500, 502, 503, 504]
  }),
  
  // Enhanced error handling
  interceptors.responseError({
    throwOnError: true,
    statusCodes: [400, 401, 403, 404, 500, 502, 503],
    includeResponseBody: true
  })
);

// All features work together automatically
const response = await httpClient.request({
  origin: 'https://api.example.com',
  path: '/data',
  method: 'GET'
});

Install with Tessl CLI

npx tessl i tessl/npm-undici

docs

caching.md

connection-management.md

cookies.md

core-http.md

errors.md

global-config.md

headers-body.md

index.md

interceptors.md

mock-testing.md

web-standards.md

tile.json