Puppeteer API Testing: Intercept, Mock, and Validate Network Requests
Puppeteer's network interception capabilities turn browser automation into an API testing tool. You can intercept requests, inspect payloads, mock responses, and validate that your frontend sends the right data to your backend — all within end-to-end test scenarios where the browser and API are exercised together.
Network Request Interception Basics
Enable request interception and handle requests:
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', (request) => {
// Allow all requests to proceed normally
request.continue();
});
await page.goto('https://app.example.com');Once interception is enabled, every request must be explicitly handled — either request.continue(), request.respond(), or request.abort(). Forgetting to handle a request causes it to hang.
Capturing API Requests
Log all API calls made during a user flow:
const apiCalls = [];
await page.setRequestInterception(true);
page.on('request', (request) => {
if (request.url().includes('/api/')) {
apiCalls.push({
method: request.method(),
url: request.url(),
headers: request.headers(),
body: request.postData(),
timestamp: Date.now(),
});
}
request.continue();
});
// Trigger the user flow
await page.goto('https://app.example.com/checkout');
await page.click('#place-order');
await page.waitForNavigation();
// Inspect what was called
console.log('API calls during checkout:', apiCalls);Validating Request Payloads
Verify the frontend sends the correct data:
const orderRequest = apiCalls.find(call =>
call.url.includes('/api/orders') && call.method === 'POST'
);
expect(orderRequest).toBeTruthy();
const body = JSON.parse(orderRequest.body);
expect(body).toMatchObject({
items: expect.arrayContaining([
expect.objectContaining({ productId: 'PROD-123', quantity: 2 })
]),
shippingAddress: expect.objectContaining({
country: 'US',
}),
});This validates frontend behavior without relying on the server to reject bad requests.
Intercepting Responses
Listen to API responses:
page.on('response', async (response) => {
if (response.url().includes('/api/')) {
const status = response.status();
let body;
try {
body = await response.json();
} catch (e) {
body = await response.text();
}
console.log(`${response.request().method()} ${response.url()} → ${status}`);
if (status >= 400) {
console.error('API error:', body);
}
}
});Important: Response bodies can only be read once. Reading the body in the response event consumes it — Puppeteer provides a buffered copy so this is handled for you, but if you try to read the body again later, you'll get an error.
Mocking API Responses
Return fake responses for isolated frontend testing:
await page.setRequestInterception(true);
page.on('request', (request) => {
if (request.url().includes('/api/products') && request.method() === 'GET') {
// Mock the response
request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
products: [
{ id: '1', name: 'Product A', price: 29.99 },
{ id: '2', name: 'Product B', price: 49.99 },
],
}),
});
} else {
request.continue();
}
});This lets you test frontend behavior independently of backend state — test the empty state, the error state, and the populated state without setting up backend data.
Mocking Error Responses
page.on('request', (request) => {
if (request.url().includes('/api/payment')) {
request.respond({
status: 422,
contentType: 'application/json',
body: JSON.stringify({
error: 'Card declined',
code: 'CARD_DECLINED',
}),
});
} else {
request.continue();
}
});
await page.goto('https://app.example.com/checkout');
await page.click('#pay-now');
// Verify error is displayed correctly
await page.waitForSelector('.payment-error');
const errorText = await page.$eval('.payment-error', el => el.textContent);
expect(errorText).toContain('Card declined');Network Failures
Simulate network errors to test frontend resilience:
page.on('request', (request) => {
if (request.url().includes('/api/search')) {
request.abort('failed'); // Simulate connection failure
} else {
request.continue();
}
});Abort reasons: 'failed', 'aborted', 'timedout', 'accessdenied', 'connectionrefused', 'connectionreset', 'internetdisconnected', 'namenotresolved', 'connectionclosed'.
Conditional Mocking by Request State
More complex scenarios require stateful mocking:
let requestCount = 0;
page.on('request', (request) => {
if (request.url().includes('/api/data')) {
requestCount++;
if (requestCount === 1) {
// First request: return loading state data
request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ status: 'processing', progress: 0 }),
});
} else if (requestCount < 5) {
// Subsequent requests: return progress
request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ status: 'processing', progress: requestCount * 25 }),
});
} else {
// Final request: return complete
request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ status: 'complete', result: 'done' }),
});
}
} else {
request.continue();
}
});Testing Authentication Flows
Intercept auth requests to verify headers and tokens:
const authHeaders = [];
page.on('request', (request) => {
const auth = request.headers()['authorization'];
if (auth) {
authHeaders.push({
url: request.url(),
header: auth,
});
}
request.continue();
});
// Perform login
await page.goto('https://app.example.com/login');
await page.type('#email', 'user@example.com');
await page.type('#password', 'password');
await page.click('#submit');
await page.waitForNavigation();
// Make an authenticated request
await page.goto('https://app.example.com/profile');
// Verify token is being sent
const profileRequest = authHeaders.find(h => h.url.includes('/api/profile'));
expect(profileRequest.header).toMatch(/^Bearer /);WebSocket Testing
Puppeteer can't intercept WebSocket frames directly, but you can monitor WebSocket connections:
page.on('request', (request) => {
if (request.resourceType() === 'websocket') {
console.log('WebSocket connection:', request.url());
}
request.continue();
});For WebSocket message inspection, use page.evaluate() to intercept at the JavaScript level:
await page.evaluateOnNewDocument(() => {
const OriginalWebSocket = window.WebSocket;
window.WebSocket = function(url, protocols) {
const ws = new OriginalWebSocket(url, protocols);
const originalSend = ws.send.bind(ws);
ws.send = function(data) {
window.__wsMessages = window.__wsMessages || [];
window.__wsMessages.push({ direction: 'sent', data, timestamp: Date.now() });
return originalSend(data);
};
ws.addEventListener('message', (event) => {
window.__wsMessages = window.__wsMessages || [];
window.__wsMessages.push({ direction: 'received', data: event.data, timestamp: Date.now() });
});
return ws;
};
Object.assign(window.WebSocket, OriginalWebSocket);
});Combining Mocks with Real Requests
Partial mocking — mock some endpoints, let others through to the real backend:
const mockEndpoints = {
'/api/feature-flags': { featureX: true, featureY: false },
'/api/experiments': { variant: 'B' },
};
page.on('request', (request) => {
const url = new URL(request.url());
const mockResponse = mockEndpoints[url.pathname];
if (mockResponse) {
request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockResponse),
});
} else {
request.continue();
}
});This lets you test feature flags and A/B test variants without setting backend state.
Timing and Latency Simulation
Simulate slow API responses to test loading states:
page.on('request', async (request) => {
if (request.url().includes('/api/search')) {
// Delay 2 seconds before responding
await new Promise(r => setTimeout(r, 2000));
request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ results: [] }),
});
} else {
request.continue();
}
});
await page.click('#search-btn');
// Loading spinner should appear
await page.waitForSelector('.loading-spinner');
const spinnerVisible = await page.$eval('.loading-spinner', el =>
getComputedStyle(el).display !== 'none'
);
expect(spinnerVisible).toBe(true);HAR (HTTP Archive) Recording
For detailed network analysis, export a HAR file:
const { PuppeteerHar } = require('puppeteer-har');
const har = new PuppeteerHar(page);
await har.start();
await page.goto('https://app.example.com');
// ... user interactions ...
const harData = await har.stop();
require('fs').writeFileSync('network.har', JSON.stringify(harData));HAR files can be opened in Chrome DevTools, Postman, or analyzed programmatically.
Continuous API Contract Testing
Mocking API responses in Puppeteer tests ensures your frontend handles specific shapes. But the real value comes from running these tests continuously — verifying that your frontend's expectations of the API stay in sync as both evolve.
HelpMeTest runs Puppeteer tests on a schedule against your real staging backend, catching API contract breaks before they reach production. Pair scheduled tests with mocked tests in your unit suite to get coverage at both layers.
Summary
Puppeteer's request interception covers the full range of API testing in browser context: capturing payloads, mocking responses, simulating errors and network failures, and validating authentication headers. The key pattern is setRequestInterception(true) + a request event handler that either continues, responds, or aborts each request based on URL matching. Combine real and mocked requests for test scenarios that are fast, reliable, and representative of real user flows.