A stand-alone types package for Undici HTTP client library
—
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.
/**
* 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;
}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" }
});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);
}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 historyAutomatic 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();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);
}
}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"
});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"
});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/responseInstall with Tessl CLI
npx tessl i tessl/npm-undici-types