A stand-alone types package for Undici HTTP client library
—
Complete mocking system for testing HTTP interactions without real network calls. Undici provides a comprehensive mock framework that allows you to intercept requests, simulate responses, and verify API interactions in your tests.
Central mock management system for controlling HTTP requests during testing.
/**
* Mock agent for intercepting and simulating HTTP requests
* Provides complete control over network behavior in tests
*/
class MockAgent extends Dispatcher {
constructor(options?: MockAgent.Options);
/** Get mock dispatcher for specific origin */
get(origin: string): MockClient;
/** Close all mock dispatchers */
close(): Promise<void>;
/** Deactivate mocking (requests pass through) */
deactivate(): void;
/** Activate mocking (requests are intercepted) */
activate(): void;
/** Enable network connections for unmatched requests */
enableNetConnect(matcher?: string | RegExp | ((origin: string) => boolean)): void;
/** Disable all network connections */
disableNetConnect(): void;
/** Get history of all mock calls */
getCallHistory(): MockCallHistory[];
/** Clear call history */
clearCallHistory(): void;
/** Enable call history tracking */
enableCallHistory(): void;
/** Disable call history tracking */
disableCallHistory(): void;
/** Get list of pending interceptors */
pendingInterceptors(): PendingInterceptor[];
/** Assert no pending interceptors remain */
assertNoPendingInterceptors(options?: {
pendingInterceptorsFormatter?: PendingInterceptorsFormatter;
}): void;
}
interface MockAgent.Options {
/** Agent options passed to underlying agent */
agent?: Agent.Options;
/** Keep alive connections during testing */
keepAliveTimeout?: number;
/** Maximum keep alive timeout */
keepAliveMaxTimeout?: number;
}
interface PendingInterceptor {
origin: string;
method: string;
path: string;
data?: any;
persist: boolean;
times: number | null;
timesInvoked: number;
error: Error | null;
}
interface PendingInterceptorsFormatter {
(pendingInterceptors: readonly PendingInterceptor[]): string;
}Usage Examples:
import { MockAgent, setGlobalDispatcher } from "undici-types";
// Basic mock agent setup
const mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);
// Enable call history for verification
mockAgent.enableCallHistory();
// Disable real network connections
mockAgent.disableNetConnect();
// Allow connections to specific origins
mockAgent.enableNetConnect("https://allowed-external-api.com");
mockAgent.enableNetConnect(/^https:\/\/.*\.trusted\.com$/);
// Test cleanup
afterEach(() => {
mockAgent.clearCallHistory();
});
afterAll(async () => {
await mockAgent.close();
});Mock dispatcher for a specific origin with request interception capabilities.
/**
* Mock client for specific origin
* Handles request interception and response simulation
*/
class MockClient extends Dispatcher {
/** Create interceptor for matching requests */
intercept(options: MockInterceptor.Options): MockInterceptor;
/** Close the mock client */
close(): Promise<void>;
/** Dispatch method (typically not called directly) */
dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean;
}
interface MockInterceptor.Options {
/** Request path (string or regex) */
path: string | RegExp | ((path: string) => boolean);
/** HTTP method */
method?: string | RegExp;
/** Request headers matcher */
headers?: Record<string, string | RegExp | ((value: string) => boolean)>;
/** Request body matcher */
body?: string | RegExp | ((body: string) => boolean);
/** Query parameters matcher */
query?: Record<string, string | RegExp | ((value: string) => boolean)>;
}
/**
* Mock interceptor for configuring responses
*/
interface MockInterceptor {
/** Return successful response */
reply<T = any>(
status: number,
data?: T,
responseOptions?: MockInterceptor.ReplyOptions
): MockScope;
/** Return response with custom function */
reply<T = any>(
replyFunction: MockInterceptor.ReplyFunction<T>
): MockScope;
/** Return error response */
replyWithError(error: Error): MockScope;
/** Default reply for unmatched requests */
defaultReplyHeaders(headers: Record<string, string>): MockInterceptor;
/** Default reply trailers */
defaultReplyTrailers(trailers: Record<string, string>): MockInterceptor;
}
interface MockInterceptor.ReplyOptions {
headers?: Record<string, string | string[]>;
trailers?: Record<string, string>;
}
interface MockInterceptor.ReplyFunction<T = any> {
(opts: {
path: string;
method: string;
body: any;
headers: Record<string, string>;
}): MockInterceptor.ReplyOptionsWithData<T>;
}
interface MockInterceptor.ReplyOptionsWithData<T = any> extends MockInterceptor.ReplyOptions {
statusCode: number;
data?: T;
}
/**
* Mock scope for controlling interceptor behavior
*/
interface MockScope {
/** Make interceptor persistent (reuse for multiple requests) */
persist(): MockScope;
/** Specify number of times interceptor should match */
times(times: number): MockScope;
/** Delay response by specified milliseconds */
delay(delay: number): MockScope;
}Usage Examples:
import { MockAgent, request } from "undici-types";
const mockAgent = new MockAgent();
const mockClient = mockAgent.get("https://api.example.com");
// Simple response mocking
mockClient
.intercept({ path: "/users", method: "GET" })
.reply(200, [
{ id: 1, name: "John Doe" },
{ id: 2, name: "Jane Smith" }
]);
// Test the mocked endpoint
const response = await request("https://api.example.com/users");
const users = await response.body.json();
console.log(users); // [{ id: 1, name: "John Doe" }, ...]
// Mock with headers
mockClient
.intercept({ path: "/protected", method: "GET" })
.reply(200, { data: "secret" }, {
headers: {
"Content-Type": "application/json",
"X-Custom-Header": "test-value"
}
});
// Mock with request matching
mockClient
.intercept({
path: "/users",
method: "POST",
headers: {
"content-type": "application/json"
},
body: (body) => {
const data = JSON.parse(body);
return data.name && data.email;
}
})
.reply(201, { id: 3, name: "New User" });
// Persistent interceptor (reused multiple times)
mockClient
.intercept({ path: "/ping", method: "GET" })
.reply(200, { status: "ok" })
.persist();
// Multiple requests to same endpoint
await request("https://api.example.com/ping"); // Works
await request("https://api.example.com/ping"); // Still works
// Limited use interceptor
mockClient
.intercept({ path: "/limited", method: "GET" })
.reply(200, { data: "limited" })
.times(2);
// Delayed response
mockClient
.intercept({ path: "/slow", method: "GET" })
.reply(200, { data: "delayed" })
.delay(1000);Mock connection pool for testing pool-specific behavior.
/**
* Mock pool for testing connection pool behavior
*/
class MockPool extends MockClient {
constructor(origin: string, options?: MockPool.Options);
}
interface MockPool.Options {
/** Agent options */
agent?: Agent.Options;
/** Mock-specific options */
connections?: number;
}Usage Examples:
import { MockPool } from "undici-types";
// Create mock pool directly
const mockPool = new MockPool("https://api.example.com", {
connections: 10
});
// Set up interceptors
mockPool
.intercept({ path: "/data", method: "GET" })
.reply(200, { items: [] });
// Use mock pool
const response = await mockPool.request({
path: "/data",
method: "GET"
});Complex response simulation and dynamic behavior.
/**
* Dynamic response function signature
*/
interface MockInterceptor.ReplyFunction<T = any> {
(opts: {
path: string;
method: string;
body: any;
headers: Record<string, string>;
query: Record<string, string>;
}): MockInterceptor.ReplyOptionsWithData<T> | Promise<MockInterceptor.ReplyOptionsWithData<T>>;
}Usage Examples:
import { MockAgent } from "undici-types";
const mockAgent = new MockAgent();
const mockClient = mockAgent.get("https://api.example.com");
// Dynamic response based on request
mockClient
.intercept({ path: /\/users\/(\d+)/, method: "GET" })
.reply((opts) => {
const userId = opts.path.match(/\/users\/(\d+)/)?.[1];
if (!userId) {
return { statusCode: 400, data: { error: "Invalid user ID" } };
}
return {
statusCode: 200,
data: { id: parseInt(userId), name: `User ${userId}` },
headers: { "X-User-ID": userId }
};
});
// Stateful mock with counter
let requestCount = 0;
mockClient
.intercept({ path: "/counter", method: "GET" })
.reply(() => {
requestCount++;
return {
statusCode: 200,
data: { count: requestCount },
headers: { "X-Request-Count": requestCount.toString() }
};
})
.persist();
// Mock with request body processing
mockClient
.intercept({ path: "/echo", method: "POST" })
.reply((opts) => {
let body;
try {
body = JSON.parse(opts.body);
} catch {
return { statusCode: 400, data: { error: "Invalid JSON" } };
}
return {
statusCode: 200,
data: {
received: body,
headers: opts.headers,
timestamp: new Date().toISOString()
}
};
});
// Conditional responses based on headers
mockClient
.intercept({ path: "/auth", method: "GET" })
.reply((opts) => {
const authHeader = opts.headers["authorization"];
if (!authHeader) {
return { statusCode: 401, data: { error: "Missing authorization" } };
}
if (authHeader === "Bearer valid-token") {
return { statusCode: 200, data: { user: "authenticated" } };
}
return { statusCode: 403, data: { error: "Invalid token" } };
});Track and verify mock interactions for test assertions.
interface MockCallHistory {
origin: string;
method: string;
path: string;
headers: Record<string, string>;
body?: any;
}
interface MockCallHistoryLog extends MockCallHistory {
timestamp: number;
duration: number;
response: {
statusCode: number;
headers: Record<string, string>;
body?: any;
};
}Usage Examples:
import { MockAgent } from "undici-types";
const mockAgent = new MockAgent();
mockAgent.enableCallHistory();
const mockClient = mockAgent.get("https://api.example.com");
// Set up mocks
mockClient
.intercept({ path: "/users", method: "GET" })
.reply(200, []);
mockClient
.intercept({ path: "/users", method: "POST" })
.reply(201, { id: 1 });
// Make requests
await request("https://api.example.com/users", { method: "GET" });
await request("https://api.example.com/users", {
method: "POST",
body: JSON.stringify({ name: "John" }),
headers: { "content-type": "application/json" }
});
// Verify call history
const history = mockAgent.getCallHistory();
expect(history).toHaveLength(2);
// Check first call
expect(history[0]).toMatchObject({
origin: "https://api.example.com",
method: "GET",
path: "/users"
});
// Check second call
expect(history[1]).toMatchObject({
origin: "https://api.example.com",
method: "POST",
path: "/users",
headers: { "content-type": "application/json" }
});
// Verify specific request was made
const postCalls = history.filter(call =>
call.method === "POST" && call.path === "/users"
);
expect(postCalls).toHaveLength(1);
expect(JSON.parse(postCalls[0].body)).toEqual({ name: "John" });Simulate network errors and failures for robust error handling testing.
/**
* Mock interceptor error simulation
*/
interface MockInterceptor {
/** Simulate network or HTTP errors */
replyWithError(error: Error): MockScope;
}Usage Examples:
import {
MockAgent,
ConnectTimeoutError,
ResponseStatusCodeError,
request
} from "undici-types";
const mockAgent = new MockAgent();
const mockClient = mockAgent.get("https://api.example.com");
// Simulate connection timeout
mockClient
.intercept({ path: "/timeout", method: "GET" })
.replyWithError(new ConnectTimeoutError("Connection timed out"));
// Simulate server error
mockClient
.intercept({ path: "/server-error", method: "GET" })
.replyWithError(new ResponseStatusCodeError(
"Internal Server Error",
500,
{
"content-type": "application/json"
},
JSON.stringify({ error: "Internal server error" })
));
// Simulate network error
mockClient
.intercept({ path: "/network-error", method: "GET" })
.replyWithError(new Error("Network unreachable"));
// Test error handling
try {
await request("https://api.example.com/timeout");
} catch (error) {
expect(error).toBeInstanceOf(ConnectTimeoutError);
}
try {
await request("https://api.example.com/server-error");
} catch (error) {
expect(error).toBeInstanceOf(ResponseStatusCodeError);
expect((error as ResponseStatusCodeError).status).toBe(500);
}
// Intermittent errors
let callCount = 0;
mockClient
.intercept({ path: "/flaky", method: "GET" })
.reply(() => {
callCount++;
if (callCount <= 2) {
throw new Error("Service temporarily unavailable");
}
return { statusCode: 200, data: { success: true } };
})
.persist();Helper functions for testing and assertion.
/**
* Assert no pending interceptors remain after test
*/
MockAgent.prototype.assertNoPendingInterceptors(options?: {
pendingInterceptorsFormatter?: PendingInterceptorsFormatter;
}): void;
/**
* Get pending interceptors that haven't been matched
*/
MockAgent.prototype.pendingInterceptors(): PendingInterceptor[];Usage Examples:
import { MockAgent } from "undici-types";
describe("API tests", () => {
let mockAgent: MockAgent;
beforeEach(() => {
mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);
mockAgent.disableNetConnect();
});
afterEach(() => {
// Ensure all mocks were used
mockAgent.assertNoPendingInterceptors({
pendingInterceptorsFormatter: (interceptors) => {
return interceptors
.map(i => `${i.method} ${i.path}`)
.join(', ');
}
});
mockAgent.clearCallHistory();
});
afterAll(async () => {
await mockAgent.close();
});
test("should use all interceptors", async () => {
const mockClient = mockAgent.get("https://api.example.com");
mockClient
.intercept({ path: "/users", method: "GET" })
.reply(200, []);
mockClient
.intercept({ path: "/posts", method: "GET" })
.reply(200, []);
// Make both requests
await request("https://api.example.com/users");
await request("https://api.example.com/posts");
// assertNoPendingInterceptors will pass in afterEach
});
test("will fail if interceptors unused", async () => {
const mockClient = mockAgent.get("https://api.example.com");
mockClient
.intercept({ path: "/users", method: "GET" })
.reply(200, []);
mockClient
.intercept({ path: "/posts", method: "GET" })
.reply(200, []); // This won't be used
// Only make one request
await request("https://api.example.com/users");
// assertNoPendingInterceptors will throw in afterEach
});
});Install with Tessl CLI
npx tessl i tessl/npm-undici-types