ScreenshotNeo

BlogHow-to

How to Mock Network Responses in Puppeteer

Use Puppeteer request interception to return controlled responses in browser tests, resolve other requests safely, and diagnose common interception errors.

By the ScreenshotNeo team4 October 20267 min read

Mock a network response in Puppeteer by enabling request interception, matching the request, and calling request.respond(). Every other intercepted request must be resolved too, usually with request.continue(), or it can remain stalled.

The examples below use the documented Puppeteer APIs. Adjust the match condition and response body to fit the endpoint your test exercises. Check the documentation for the Puppeteer version installed in your project, since APIs can change.

1. Enable interception and return a mock response

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  const mockUrl = 'https://example.test/api/data';

  await page.setRequestInterception(true);
  page.on('request', request => {
    if (request.url() === mockUrl) {
      return request.respond({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify({ ok: true, items: ['alpha', 'beta'] }),
      });
    }

    return request.continue();
  });

  await page.goto('https://example.test');
  // Assert on the page or application state that uses /api/data.
} finally {
  await browser.close();
}

This is a complete ES module example. It assumes Puppeteer is installed in the project and that the page under test makes the matching request. The example URL and payload are illustrative. See Puppeteer’s HTTPRequest.respond() API, Request Interception guide, and Page.setRequestInterception() API.

Match the intended request

Matching the full URL is precise, but query parameters or changing identifiers may make it too strict. You can match method, URL, or resource type as needed:

page.on('request', request => {
  const url = new URL(request.url());
  const isTarget = url.origin === 'https://example.test'
    && url.pathname === '/api/data'
    && request.method() === 'GET';

  if (isTarget) {
    return request.respond({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ ok: true }),
    });
  }

  return request.continue();
});

Only use broad matching when the test intends to replace every request in that category. A hostname plus path and method often avoids accidentally mocking a similarly named endpoint.

2. Choose the mock response fields

request.respond() fulfills the intercepted request with the response you supply. The useful fields for typical tests include:

Field Purpose Example
status HTTP status returned to the page. 200, 404, 503
contentType Content type of the body. application/json, text/plain
body Response content as a string. JSON.stringify(data)

Mock JSON

return request.respond({
  status: 200,
  contentType: 'application/json',
  body: JSON.stringify({ userId: 42, name: 'Sample User' }),
});

Mock an HTTP error response

return request.respond({
  status: 404,
  contentType: 'text/plain',
  body: 'Not Found!',
});

A 404 or 503 is still an HTTP response. Puppeteer emits requestfinished for these HTTP error responses; they do not become requestfailed merely because the status is an error. A transport failure is a different event. See the PageEvent documentation.

3. Make the mock deterministic in tests

Install the interception handler before navigating or triggering the application action that makes the request. Then wait for the page behavior your test cares about, rather than relying on an arbitrary delay.

await page.setRequestInterception(true);
page.on('request', request => {
  if (request.url().includes('/api/data')) {
    return request.respond({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ ready: true }),
    });
  }
  return request.continue();
});

await page.goto('https://example.test');
await page.waitForSelector('[data-testid="loaded-data"]');

The selector is application-specific. If the test is about an error state, mock that status and wait for the error UI instead. Keep each test’s mock data focused on the behavior under test so failures remain easy to interpret.

Observe requests and responses

Puppeteer emits request and response events by default. Logging them can help distinguish a request that never happened from one that received an unexpected response:

page.on('request', request => {
  console.log('request', request.method(), request.url());
});
page.on('response', response => {
  console.log('response', response.status(), response.url());
});
page.on('requestfailed', request => {
  console.log('failed', request.url(), request.failure()?.errorText);
});

See Puppeteer’s Network logging guide for request and response event details.

4. Safely handle multiple interception listeners

Once interception is enabled, each request stalls until it is continued, responded to, or aborted, except requests completed by the browser cache. Every request needs a resolution. In a project with multiple listeners or a package that also intercepts requests, another handler may resolve a request first.

page.on('request', request => {
  if (request.isInterceptResolutionHandled()) return;

  if (request.url() === 'https://example.test/api/data') {
    return request.respond({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ ok: true }),
    });
  }

  return request.continue();
});

If a handler performs asynchronous work before resolving the request, check again afterward. Another listener may have acted while the handler was awaiting. Keep the final check and resolution together synchronously:

page.on('request', async request => {
  if (request.isInterceptResolutionHandled()) return;

  const shouldMock = await decideWhetherToMock(request.url());

  // Another listener may have resolved it during the await.
  if (request.isInterceptResolutionHandled()) return;

  if (shouldMock) {
    return request.respond({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ mocked: true }),
    });
  }

  return request.continue();
});

Replace decideWhetherToMock with your own asynchronous decision function. If another handler has already resolved the request, this handler exits without attempting a second resolution.

Cooperative interception priorities

Puppeteer also documents Cooperative Intercept Mode for setups where multiple handlers intentionally collaborate. When every resolving handler supplies a numeric priority, handlers are awaited and the highest-priority resolution wins. On equal priorities, abort outranks respond, which outranks continue. If any handler omits a priority, legacy behavior applies and a resolution may happen immediately. For a single handler, ordinary respond() and continue() calls are simpler. Consult the current official guide before combining interception packages.

5. Troubleshooting

Symptom Likely cause Fix
Navigation or page activity hangs. An intercepted request was not continued, responded to, or aborted. Ensure the handler resolves the matched request and has a fallback resolution for all others.
Request is already handled! A second listener tried to resolve an interception that was already handled. Check request.isInterceptResolutionHandled() before resolving; check again after every await.
The real response appears instead of the mock. The handler was attached after the request, the URL condition did not match, or the request was served from cache. Enable interception and register the listener before triggering the request; log the actual URL and method; inspect query strings and cache behavior.
Mocking a data URL has no effect. request.respond() on a data URL is documented as a no-op; mocking data URLs is unsupported. Mock a network URL instead, or set up the page content through another mechanism appropriate to the test.
The test treats a mocked 404 as a failed network request. An HTTP error status is being confused with a transport-level failure. Handle the response/status for 404 or 503. Use requestfailed for transport failures.
Mock triggers for too many requests. The condition matches by substring or path without checking host, method, or query context. Use a URL parser and match origin, pathname, and method explicitly.

6. Performance, reliability, and cost considerations

  • Performance: Interception pauses requests while handlers decide how to resolve them. Keep handlers small and avoid unnecessary asynchronous work. Mock only the traffic relevant to the test when possible.
  • Reliability: Register handlers before navigation, resolve every request, and avoid competing handlers unless you understand their resolution behavior. Recheck handling after awaits.
  • Repeatability: Return stable fixtures and explicit status and content type values. Avoid depending on a live API when the test is intended to isolate frontend behavior.
  • Cost: Puppeteer interception itself has no ScreenshotNeo charge. Costs can come from the browser environment, CI runtime, or services your test calls if you allow unmatched traffic to continue.

7. Or skip the browser setup

If your goal is to capture a website image or PDF rather than test application behavior, ScreenshotNeo provides a screenshot API and MCP server. A GET request returns a PNG, JPEG, WebP, or PDF. Cookie banners are accepted like a visitor and more than 60 known consent platforms, newsletter popups, and chat widgets are removed before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. AI agents can use the MCP tools take_screenshot, get_page_info, and capture_pdf.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo API documentation for the request options. It also supports full-page and element capture, viewport and device presets, PDF settings, custom CSS and JavaScript, wait conditions, request blocking, cookies and headers, caching, signed image links, asynchronous jobs, bulk capture, and a usage API. Plans include 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 shots. All features are on every plan.

Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.

8. FAQ

Can I mock only one API endpoint?

Yes. Match the request URL, and preferably its origin, path, and method, then continue every request that does not match.

Does a mocked 404 mean the request failed?

No. It is an HTTP response and Puppeteer reports it as requestfinished. A transport-level failure uses requestfailed.

Can I intercept requests without changing the real server?

Yes. Request interception supplies the response in the browser, so the server does not need to return that mocked payload.

Where should I verify API behavior?

Check the official Puppeteer API and guide for the version in your project, especially if you use multiple interception handlers.