Mocking the Network in Puppeteer: Deterministic Tests Without a Backend
Flaky staging APIs make flaky UI tests. Puppeteer's request interception lets you mock, block, or fail HTTP traffic from inside your test process — no backend required.
10 Jun 2026, 12:44 UTC

Your UI test suite is flaky because the staging API is flaky. The fix isn't a more reliable staging environment — it's cutting the network out of the loop entirely. Puppeteer's request interception API lets you answer HTTP requests from inside your test process, so the page under test gets exactly the responses you decide, every time.
How interception works
Call await page.setRequestInterception(true) and every outgoing request from that page — navigations, subresources, XHR, and (since Puppeteer 19 / Chrome 106) fetch() calls — is paused at the Chrome DevTools Protocol Network layer and surfaced as a request event. Your handler must settle each request with one of three verbs:
request.continue()— let it through, optionally with modified headers or POST data.request.abort()— fail it, optionally with an error code like'internetdisconnected'.request.respond()— answer it yourself with a status, headers, and body.
That third option is the workhorse for mocking: no test server, no fixture proxy, just an object literal.
A worked example: mocking a JSON API
Run this in Node (Puppeteer 19+, any Chromium 106+). It intercepts calls to /api/user and returns canned JSON, then lets everything else through:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', (request) => {
if (request.url().includes('/api/user')) {
return request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ name: 'Ada Lovelace', plan: 'pro' }),
});
}
request.continue();
});
await page.goto('https://your-app.example/dashboard');
const name = await page.$eval('[data-testid="user-name"]', el => el.textContent);
console.assert(name === 'Ada Lovelace', 'UI rendered mocked data');
await browser.close();No special permissions are needed beyond normal Node execution. To verify the handler actually fires, run with DEBUG=puppeteer:* and watch for CDP Network.requestIntercepted messages in the logs. If the page hangs instead, your handler likely missed a branch — every request must be continued, aborted, or responded to, or it stalls until the navigation timeout (30s by default, adjustable via page.setDefaultNavigationTimeout).
Blocking and failure simulation
The same mechanism handles two other common needs. To speed up tests, abort requests you don't care about — analytics, fonts, third-party trackers:
page.on('request', (request) => {
const blocked = ['image', 'font', 'media'].includes(request.resourceType());
blocked ? request.abort() : request.continue();
});To test error states, abort the API call with request.abort('internetdisconnected') and assert your UI shows its offline banner. Note this is a hard failure — the browser won't retry, which is usually what you want for a test but differs from real flaky-network behavior.
Trade-offs worth knowing
Interception isn't free. Enabling it disables the browser cache for the page, so every resource re-fetches on each run — expect slower tests and more bandwidth. Handlers execute in your Node process, not the browser, so heavy synchronous work inside a handler blocks the event loop and can trigger navigation timeouts. Cross-origin iframes are another edge: with site isolation, their requests may live in a separate CDP session and never reach your handler. If you rely on intercepting iframe traffic, verify it against your Chrome version by logging request URLs from a page with an external frame.
For parallel suites, use browser.createBrowserContext() to give each test its own isolated context so mock rules don't leak between tests.
Closing
If your tests depend on a backend you don't control, move the dependency into the test: enable interception, respond with fixtures, and abort everything irrelevant. Start with one deterministic endpoint mock, confirm it with DEBUG=puppeteer:*, and expand from there. Check the Puppeteer release notes for your pinned version before relying on newer helpers like stream-based responses.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.