CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-undici-types

A stand-alone types package for Undici HTTP client library

Pending
Overview
Eval results
Files

interceptors.mddocs/

Interceptors

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.

Capabilities

Core Interceptor Interface

/**
 * Base interceptor interface for request/response transformation
 */
interface Interceptor {
  (dispatch: Dispatcher['dispatch']): Dispatcher['dispatch'];
}

/**
 * Interceptor options base interface
 */
interface InterceptorOptions {
  /** Maximum number of redirects/retries */
  maxRedirections?: number;
}

Dump Interceptor

Captures and logs request/response data for debugging and monitoring.

/**
 * Creates interceptor that captures request/response data
 * @param options - Dump configuration options
 * @returns Interceptor function
 */
function dump(options?: DumpInterceptorOpts): Interceptor;

interface DumpInterceptorOpts {
  /** Maximum body size to capture (bytes) */
  maxSize?: number;
  
  /** Whether to capture request body */
  captureRequestBody?: boolean;
  
  /** Whether to capture response body */
  captureResponseBody?: boolean;
  
  /** Custom logging function */
  logger?: (data: DumpData) => void;
}

interface DumpData {
  request: {
    origin: string;
    method: string;
    path: string;
    headers: Record<string, string | string[]>;
    body?: string | Buffer;
  };
  response: {
    statusCode: number;
    headers: Record<string, string | string[]>;
    body?: string | Buffer;
  };
  timestamp: number;
  duration: number;
}

Usage Examples:

import { Client, interceptors } from "undici-types";

// Basic dump interceptor
const client = new Client("https://api.example.com")
  .compose(interceptors.dump());

// Request will be logged to console
const response = await client.request({
  path: "/users",
  method: "GET"
});

// Dump with custom options
const dumpClient = new Client("https://api.example.com")
  .compose(interceptors.dump({
    maxSize: 10240, // 10KB max
    captureRequestBody: true,
    captureResponseBody: true,
    logger: (data) => {
      console.log(`${data.request.method} ${data.request.path} - ${data.response.statusCode} (${data.duration}ms)`);
      console.log("Request headers:", data.request.headers);
      console.log("Response headers:", data.response.headers);
      
      if (data.request.body) {
        console.log("Request body:", data.request.body.toString());
      }
      
      if (data.response.body) {
        console.log("Response body:", data.response.body.toString());
      }
    }
  }));

// Make requests with detailed logging
await dumpClient.request({
  path: "/users",
  method: "POST",
  body: JSON.stringify({ name: "John Doe" }),
  headers: { "content-type": "application/json" }
});

Retry Interceptor

Automatic retry logic for failed requests with configurable strategies.

/**
 * Creates interceptor that retries failed requests
 * @param options - Retry configuration options
 * @returns Interceptor function
 */
function retry(options?: RetryInterceptorOpts): Interceptor;

interface RetryInterceptorOpts extends InterceptorOptions {
  /** Number of retry attempts */
  retry?: number;
  
  /** HTTP methods to retry */
  methods?: HttpMethod[];
  
  /** HTTP status codes to retry */
  statusCodes?: number[];
  
  /** Error codes to retry */
  errorCodes?: string[];
  
  /** Minimum delay between retries (ms) */
  minTimeout?: number;
  
  /** Maximum delay between retries (ms) */
  maxTimeout?: number;
  
  /** Multiplier for exponential backoff */
  timeoutFactor?: number;
  
  /** Maximum delay from Retry-After header (ms) */
  maxRetryAfter?: number;
  
  /** Whether to respect Retry-After headers */
  retryAfter?: boolean;
  
  /** Custom retry condition function */
  retryCondition?: (error: Error, context: RetryContext) => boolean;
}

interface RetryContext {
  attempt: number;
  maxAttempts: number;
  error: Error;
  request: {
    method: string;
    path: string;
    headers: Record<string, string | string[]>;
  };
}

type HttpMethod = "GET" | "HEAD" | "OPTIONS" | "PUT" | "DELETE" | "TRACE";

Usage Examples:

import { Client, interceptors } from "undici-types";

// Basic retry interceptor
const retryClient = new Client("https://unreliable-api.example.com")
  .compose(interceptors.retry({
    retry: 3,
    methods: ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"],
    statusCodes: [408, 413, 429, 500, 502, 503, 504],
    errorCodes: ["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ENETDOWN"]
  }));

// Advanced retry with custom backoff
const advancedRetryClient = new Client("https://api.example.com")
  .compose(interceptors.retry({
    retry: 5,
    minTimeout: 1000,
    maxTimeout: 30000,
    timeoutFactor: 2,
    retryAfter: true, // Respect Retry-After headers
    maxRetryAfter: 60000,
    retryCondition: (error, context) => {
      // Custom retry logic
      if (context.attempt >= 3 && error.message.includes("rate limit")) {
        return false; // Don't retry rate limits after 3 attempts
      }
      
      // Default retry for network errors
      return context.attempt < context.maxAttempts;
    }
  }));

// Requests automatically retry on failure
try {
  const response = await retryClient.request({
    path: "/flaky-endpoint",
    method: "GET"
  });
} catch (error) {
  // Error thrown only after all retry attempts failed
  console.error("Request failed after retries:", error);
}

Redirect Interceptor

Automatic handling of HTTP redirects with security controls.

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

interface RedirectInterceptorOpts extends InterceptorOptions {
  /** Maximum number of redirects to follow */
  maxRedirections?: number;
  
  /** Whether to preserve request body on redirects */
  throwOnMaxRedirect?: boolean;
  
  /** Custom redirect validation function */
  beforeRedirect?: (options: {
    headers: Record<string, string | string[]>;
    statusCode: number;
    location: string;
    opaque: unknown;
  }) => void;
  
  /** HTTP methods allowed for redirects */
  allowedMethods?: HttpMethod[];
  
  /** Whether to follow redirects to different origins */
  allowCrossOrigin?: boolean;
}

Usage Examples:

import { Client, interceptors } from "undici-types";

// Basic redirect interceptor
const redirectClient = new Client("https://api.example.com")
  .compose(interceptors.redirect({
    maxRedirections: 10
  }));

// Secure redirect handling
const secureRedirectClient = new Client("https://api.example.com")
  .compose(interceptors.redirect({
    maxRedirections: 5,
    allowCrossOrigin: false, // Don't follow cross-origin redirects
    allowedMethods: ["GET", "HEAD"], // Only follow redirects for safe methods
    beforeRedirect: ({ headers, statusCode, location }) => {
      console.log(`Redirecting ${statusCode} to ${location}`);
      
      // Validate redirect destination
      const url = new URL(location);
      if (!url.hostname.endsWith('.trusted-domain.com')) {
        throw new Error('Redirect to untrusted domain blocked');
      }
    },
    throwOnMaxRedirect: true
  }));

// Custom redirect logging
const loggingRedirectClient = new Client("https://api.example.com")
  .compose(interceptors.redirect({
    maxRedirections: 3,
    beforeRedirect: ({ statusCode, location, headers }) => {
      console.log(`Following ${statusCode} redirect to: ${location}`);
      
      // Log redirect chain for debugging
      const cacheControl = headers['cache-control'];
      if (cacheControl) {
        console.log(`Cache-Control: ${cacheControl}`);
      }
    }
  }));

// Request follows redirects automatically
const response = await redirectClient.request({
  path: "/redirect-me",
  method: "GET"
});

console.log(`Final URL: ${response.context.history}`); // Redirect history

Decompression Interceptor

Automatic decompression of compressed response bodies.

/**
 * Creates interceptor that decompresses response bodies
 * @param options - Decompression configuration options
 * @returns Interceptor function
 */
function decompress(options?: DecompressInterceptorOpts): Interceptor;

interface DecompressInterceptorOpts {
  /** Compression formats to support */
  supportedEncodings?: string[];
  
  /** Maximum decompressed size (bytes) */
  maxSize?: number;
  
  /** Whether to throw on unsupported encoding */
  throwOnUnsupportedEncoding?: boolean;
}

Usage Examples:

import { Client, interceptors } from "undici-types";

// Basic decompression
const decompressClient = new Client("https://api.example.com")
  .compose(interceptors.decompress());

// Custom decompression options
const customDecompressClient = new Client("https://api.example.com")
  .compose(interceptors.decompress({
    supportedEncodings: ["gzip", "deflate", "br"], // Brotli support
    maxSize: 50 * 1024 * 1024, // 50MB max decompressed size
    throwOnUnsupportedEncoding: false
  }));

// Automatically handles compressed responses
const response = await decompressClient.request({
  path: "/compressed-data",
  method: "GET",
  headers: {
    "accept-encoding": "gzip, deflate, br"
  }
});

// Response body is automatically decompressed
const data = await response.body.text();

Response Error Interceptor

Automatic error throwing for HTTP error status codes.

/**
 * Creates interceptor that throws errors for HTTP error status codes
 * @param options - Response error configuration options
 * @returns Interceptor function
 */
function responseError(options?: ResponseErrorInterceptorOpts): Interceptor;

interface ResponseErrorInterceptorOpts {
  /** Status codes that should throw errors */
  statusCodes?: number[];
  
  /** Whether to include response body in error */
  includeBody?: boolean;
  
  /** Custom error factory function */
  errorFactory?: (response: {
    statusCode: number;
    headers: Record<string, string | string[]>;
    body: any;
  }) => Error;
}

Usage Examples:

import { Client, interceptors, ResponseStatusCodeError } from "undici-types";

// Basic error throwing for 4xx/5xx status codes
const errorClient = new Client("https://api.example.com")
  .compose(interceptors.responseError({
    statusCodes: [400, 401, 403, 404, 500, 502, 503, 504]
  }));

// Custom error handling
const customErrorClient = new Client("https://api.example.com")
  .compose(interceptors.responseError({
    includeBody: true,
    errorFactory: ({ statusCode, headers, body }) => {
      if (statusCode === 401) {
        return new Error(`Authentication failed: ${body.message}`);
      }
      
      if (statusCode === 429) {
        const retryAfter = headers['retry-after'];
        return new Error(`Rate limited. Retry after: ${retryAfter}s`);
      }
      
      return new ResponseStatusCodeError(
        `HTTP ${statusCode}`,
        statusCode,
        headers,
        body
      );
    }
  }));

// Requests throw errors for non-success status codes
try {
  const response = await errorClient.request({
    path: "/protected-resource",
    method: "GET"
  });
} catch (error) {
  if (error instanceof ResponseStatusCodeError) {
    console.error(`HTTP Error: ${error.status} ${error.statusText}`);
    console.error("Response body:", error.body);
  }
}

DNS Interceptor

Custom DNS resolution for requests.

/**
 * Creates interceptor that customizes DNS resolution
 * @param options - DNS configuration options
 * @returns Interceptor function
 */
function dns(options: DNSInterceptorOpts): Interceptor;

interface DNSInterceptorOpts {
  /** Maximum TTL for cached DNS entries */
  maxTTL?: number;
  
  /** Maximum number of cached items */
  maxItems?: number;
  
  /** Custom DNS lookup function */
  lookup?: (
    hostname: string,
    options: LookupOptions,
    callback: (err: NodeJS.ErrnoException | null, addresses: DNSInterceptorRecord[]) => void
  ) => void;
  
  /** Custom pick function for selecting from multiple records */
  pick?: (origin: URL, records: DNSInterceptorOriginRecords, affinity: 4 | 6) => DNSInterceptorRecord;
  
  /** Enable dual stack (IPv4 and IPv6) */
  dualStack?: boolean;
  
  /** IP version affinity */
  affinity?: 4 | 6;
}

interface DNSInterceptorOriginRecords {
  4: { ips: DNSInterceptorRecord[] } | null;
  6: { ips: DNSInterceptorRecord[] } | null;
}

interface DNSInterceptorRecord {
  address: string;
  ttl: number;
  family: 4 | 6;
}

interface LookupOptions {
  family?: 4 | 6 | 0;
  hints?: number;
  all?: boolean;
}

Usage Examples:

import { Client, interceptors } from "undici-types";

// DNS interceptor with caching
const dnsClient = new Client("https://api.example.com")
  .compose(interceptors.dns({
    maxTTL: 300000, // 5 minute max TTL
    maxItems: 100,  // Cache up to 100 entries
    dualStack: true,
    affinity: 4 // Prefer IPv4
  }));

// Custom DNS lookup
const customDnsClient = new Client("https://api.example.com")
  .compose(interceptors.dns({
    lookup: (hostname, options, callback) => {
      // Custom DNS resolution logic
      if (hostname === "api.example.com") {
        callback(null, [{
          address: "192.168.1.100",
          ttl: 300,
          family: 4
        }]);
      } else {
        // Fallback to system DNS
        require("dns").lookup(hostname, options, (err, address, family) => {
          if (err) return callback(err, []);
          callback(null, [{
            address,
            ttl: 300,
            family: family as 4 | 6
          }]);
        });
      }
    }
  }));

// Requests use custom DNS resolution
const response = await dnsClient.request({
  path: "/data",
  method: "GET"
});

Cache Interceptor

HTTP response caching with RFC 7234 compliance.

/**
 * Creates interceptor that caches HTTP responses
 * @param options - Cache configuration options
 * @returns Interceptor function
 */
function cache(options?: CacheInterceptorOpts): Interceptor;

interface CacheInterceptorOpts {
  /** Cache store implementation */
  store?: CacheStore;
  
  /** Cache methods */
  methods?: string[];
  
  /** Maximum cache age in milliseconds */
  maxAge?: number;
  
  /** Whether to cache responses with no explicit cache headers */
  cacheDefault?: boolean;
  
  /** Custom cache key generation */
  generateCacheKey?: (request: {
    origin: string;
    method: string;
    path: string;
    headers: Record<string, string | string[]>;
  }) => string;
}

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

interface CacheValue {
  statusCode: number;
  headers: Record<string, string | string[]>;
  body: Buffer;
  cachedAt: number;
}

class MemoryCacheStore implements CacheStore {
  constructor(opts?: MemoryCacheStoreOpts);
  get(key: string): Promise<CacheValue | null>;
  set(key: string, value: CacheValue, ttl?: number): Promise<void>;
  delete(key: string): Promise<boolean>;
  clear(): Promise<void>;
}

interface MemoryCacheStoreOpts {
  maxItems?: number;
  maxEntrySize?: number;
}

class SqliteCacheStore implements CacheStore {
  constructor(opts?: SqliteCacheStoreOpts);
  get(key: string): Promise<CacheValue | null>;
  set(key: string, value: CacheValue, ttl?: number): Promise<void>;
  delete(key: string): Promise<boolean>;
  clear(): Promise<void>;
}

interface SqliteCacheStoreOpts {
  location?: string;
  maxCount?: number;
  maxSize?: number;
  maxEntrySize?: number;
}

Usage Examples:

import { Client, interceptors, MemoryCacheStore, SqliteCacheStore } from "undici-types";

// Basic in-memory caching
const cacheClient = new Client("https://api.example.com")
  .compose(interceptors.cache({
    store: new MemoryCacheStore({
      maxItems: 1000,
      maxEntrySize: 1024 * 1024 // 1MB max per entry
    }),
    methods: ["GET", "HEAD"],
    cacheByDefault: 300 // Cache for 5 minutes by default
  }));

// SQLite-based persistent caching
const persistentCacheClient = new Client("https://api.example.com")
  .compose(interceptors.cache({
    store: new SqliteCacheStore({
      location: "./cache.db",
      maxCount: 10000,
      maxSize: 100 * 1024 * 1024, // 100MB total cache size
      maxEntrySize: 5 * 1024 * 1024 // 5MB max per entry
    }),
    methods: ["GET", "HEAD", "OPTIONS"]
  }));

// Custom cache key generation
const customCacheClient = new Client("https://api.example.com")
  .compose(interceptors.cache({
    store: new MemoryCacheStore(),
    generateCacheKey: ({ origin, method, path, headers }) => {
      const userId = headers["x-user-id"];
      return `${method}:${origin}${path}:${userId}`;
    }
  }));

// First request fetches from server
const response1 = await cacheClient.request({
  path: "/data",
  method: "GET"
});

// Second request served from cache
const response2 = await cacheClient.request({
  path: "/data", 
  method: "GET"
});

Interceptor Composition

Combining multiple interceptors for comprehensive request/response handling.

Usage Examples:

import { Client, interceptors } from "undici-types";

// Compose multiple interceptors
const enhancedClient = new Client("https://api.example.com")
  .compose(interceptors.dns({
    origins: {
      "https://api.example.com": [{ address: "10.0.0.1", family: 4 }]
    }
  }))
  .compose(interceptors.retry({
    retry: 3,
    methods: ["GET", "HEAD", "PUT", "DELETE"]
  }))
  .compose(interceptors.redirect({
    maxRedirections: 5
  }))
  .compose(interceptors.decompress())
  .compose(interceptors.responseError({
    statusCodes: [400, 401, 403, 404, 500, 502, 503, 504]
  }))
  .compose(interceptors.cache({
    store: new MemoryCacheStore(),
    methods: ["GET", "HEAD"]
  }))
  .compose(interceptors.dump({
    logger: (data) => console.log(`${data.request.method} ${data.request.path} - ${data.response.statusCode}`)
  }));

// All interceptors applied in composition order
const response = await enhancedClient.request({
  path: "/api/resource",
  method: "GET"
});

// Request flow:
// 1. DNS resolution (custom IP)
// 2. Retry on failure
// 3. Follow redirects  
// 4. Decompress response
// 5. Throw on error status
// 6. Cache successful response
// 7. Log request/response

Install with Tessl CLI

npx tessl i tessl/npm-undici-types

docs

connection-management.md

cookies.md

error-handling.md

http-api.md

http-clients.md

index.md

interceptors.md

testing-mocking.md

utilities.md

web-standards.md

tile.json