ScreenshotNeo

BlogHow-to

How to Continue a Puppeteer Request with Overrides

Learn how to continue intercepted Puppeteer requests with header, method, body, and URL overrides, while avoiding stalled requests and duplicate resolution.

By the ScreenshotNeo team4 October 20269 min read

To continue an intercepted Puppeteer request with overrides, enable request interception before the request occurs, then call request.continue() with only the properties you want to change. Puppeteer supports optional headers, method, postData, and url overrides. Every intercepted request must be resolved, or it can stall.

The examples below use JavaScript and Puppeteer. The API details are based on Puppeteer’s official request interception guide and API reference: network interception guide, HTTPRequest.continue(), and ContinueRequestOverrides. These pages identify the API versions as 25.12.0 and 25.10.0, respectively; check the docs for the version installed in your project.

1. Minimal working example

This runnable script adds a request header to every request and continues each one. Save it as continue.js in a project where Puppeteer is installed, then run node continue.js.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();

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

      request.continue({
        headers: {
          ...request.headers(),
          'x-example': 'value',
        },
      });
    });

    await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
    console.log(await page.title());
  } finally {
    await browser.close();
  }
})();

setRequestInterception(true) activates interception actions such as continue(), abort(), and respond(). Without it, calling continue() throws. The handler checks whether another listener has already resolved the request, then preserves existing headers while adding one new header. Puppeteer returns lower-case header names from request.headers().

2. Choose the override you need

Override Use it for Important detail
headers Adding, replacing, or removing request headers Copy existing headers if you want to preserve them; use an undefined value to remove a header.
method Changing the HTTP method, such as GET to POST Changing a method may require a compatible body and server behavior.
postData Supplying or replacing the request body Ensure the body encoding and any content-type header match.
url Sending the request to a different URL This changes the request URL; Puppeteer documents that it is not a redirect.

All four fields are optional. You can combine them in one call. Avoid setting fields you do not need to change: leaving them out lets the intercepted request retain its existing value.

Add, replace, or remove headers

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

  const headers = {
    ...request.headers(),
    'x-example': 'value',
    'x-remove-this': undefined,
  };

  request.continue({ headers });
});

Header names are lower-case in the object returned by request.headers(). Copying that object avoids unintentionally dropping headers. Puppeteer’s documented header example uses undefined to remove a header.

Change the method and body

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

  request.continue({
    method: 'POST',
    postData: JSON.stringify({ source: 'puppeteer' }),
    headers: {
      ...request.headers(),
      'content-type': 'application/json',
    },
  });
});

Use this only for requests where changing the method and body makes sense. A server may reject an unexpected method or payload. If you change the body format, set a matching content type where needed.

Change the URL

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

  request.continue({ url: 'https://example.com/alternate-path' });
});

This overrides the URL used for that request. It does not create an HTTP redirect or update the page’s address bar as a redirect would. If you need to observe or follow redirect behavior, handle the server’s redirect response and inspect the resulting requests separately.

3. Match only the requests you intend to change

Interception runs for requests from the page, including assets and other subresources. Filter by URL or resource type, and still continue requests that do not match your special case.

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

  const url = request.url();
  if (url.startsWith('https://example.com/api/')) {
    request.continue({
      headers: {
        ...request.headers(),
        'x-api-source': 'browser-test',
      },
    });
    return;
  }

  // Non-matching requests must also be resolved.
  request.continue();
});

Returning from a handler without resolving a request leaves it waiting. The early return in this pattern is safe only because the request was already resolved by another handler, as confirmed by isInterceptResolutionHandled().

4. Avoid duplicate resolution when handlers overlap

A request may be seen by multiple listeners. Before calling continue(), abort(), or respond(), check request.isInterceptResolutionHandled(). If the handler awaits asynchronous work, check again immediately before resolving because another listener may have handled the request while it was waiting.

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

  const shouldAddHeader = await decideWhetherToModify(request.url());

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

  if (shouldAddHeader) {
    request.continue({
      headers: {
        ...request.headers(),
        'x-example': 'value',
      },
    });
  } else {
    request.continue();
  }
});

async function decideWhetherToModify(url) {
  return url.includes('/api/');
}

The asynchronous function is deliberately simple; replace it with your own decision logic. Keep it bounded and dependable, since the intercepted request remains pending while the handler waits.

Cooperative interception priorities

Puppeteer supports cooperative handling when every handler that resolves a request supplies a numeric priority. In that mode, the highest priority wins. For equal priorities, abort takes precedence over respond, which takes precedence over continue. If any handler omits the priority, legacy immediate resolution applies instead.

For a continuation that simply passes the request along, Puppeteer’s guide recommends the default priority of 0 when using cooperative handling:

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

Use one consistent approach across participating handlers. Mixing priority-based calls with calls that omit priorities can cause a handler to resolve immediately before the other handlers’ priorities are considered. See Puppeteer’s interception guide for the resolution model.

5. Complete example: override one API request

This example modifies only matching API requests, keeps the rest of the page working, waits for DOM content, and closes the browser even if navigation fails.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    await page.setRequestInterception(true);

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

      if (request.url().startsWith('https://example.com/api/')) {
        request.continue({
          headers: {
            ...request.headers(),
            'x-client-kind': 'automation',
          },
        });
      } else {
        request.continue();
      }
    });

    const response = await page.goto('https://example.com', {
      waitUntil: 'domcontentloaded',
      timeout: 30000,
    });

    console.log({
      status: response ? response.status() : null,
      title: await page.title(),
    });
  } finally {
    await browser.close();
  }
})();

domcontentloaded can be appropriate when you need the document to load but do not need every image or long-lived connection to finish. Select a wait condition that matches your actual task. If you need the response from a particular API call, wait for that request or response explicitly rather than assuming page navigation completion means the API finished.

6. Request body edge cases

request.postData() may be unavailable even when a request has POST data, for example when the body is too long or cannot readily be decoded. Puppeteer’s request reference provides fetchPostData() for such cases. If you need to inspect or transform a body, account for asynchronous retrieval and check that the request remains unresolved before continuing it.

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

  let body = request.postData();
  if (body === undefined) {
    body = await request.fetchPostData();
  }

  if (request.isInterceptResolutionHandled()) return;

  request.continue({
    postData: body,
    headers: request.headers(),
  });
});

Use this pattern only for requests where preserving or changing a body is relevant. See Puppeteer’s HTTPRequest API for the body access methods and their behavior.

7. Troubleshooting

Symptom Likely cause Fix
Request is already handled or an interception resolution error Another listener resolved the request first, or the handler resumed after asynchronous work without rechecking. Check isInterceptResolutionHandled() before resolving and again after every await.
Navigation or resources hang after enabling interception At least one intercepted request was never continued, responded to, or aborted. Ensure every branch resolves its request. Continue requests that do not match your filter.
continue() throws when called Request interception was not enabled for the page. Call await page.setRequestInterception(true) before the relevant requests happen.
Your new header is present but other headers disappeared The override replaced the header set with an incomplete object. Start with { ...request.headers() }, then edit the fields you need.
Changing a URL did not cause a redirect A URL override changes the outgoing request URL; it is not an HTTP redirect. Use the URL override when you want to target a different URL. Use server redirect behavior when you need a redirect.
The original request body is unavailable postData() may not expose a long or not readily decoded body. Use await request.fetchPostData(), then recheck the handled state before resolving.
A request modification appears inconsistent across listeners Handlers are mixing cooperative priorities with legacy immediate resolution. Coordinate handlers to use numeric priorities consistently, or use legacy handling consistently. In cooperative mode, the highest priority wins.
Server rejects the modified request The method, body, headers, or URL no longer match what the endpoint accepts. Verify the endpoint’s expected method and payload format; keep content type aligned with the body.

8. Performance, reliability, and cost

Performance

  • Interception adds a handler decision to each request. Keep filters inexpensive and avoid doing unrelated asynchronous work in the request event.
  • Only fetch or parse request bodies for requests you intend to inspect. Body retrieval and remote decision logic can keep a request pending.
  • Choose a navigation wait condition that matches the required result. Waiting for all network activity can be unsuitable for pages with ongoing connections; waiting only for DOM content may be too early for data rendered later.

Reliability

  • Install interception and listeners before navigation so the initial document and its early subrequests are covered.
  • Resolve all requests, including non-matching requests, and guard against duplicate resolution.
  • Use try/finally around browser lifetime management so failures do not leave a browser process open.
  • Set a navigation timeout and wait for the particular response your workflow needs instead of relying on an overly broad page-load condition.

Cost

Puppeteer is browser automation software; the supplied Puppeteer documentation does not establish a service price or per-request cost. Your actual cost depends on where and how you run the browser, including compute, memory, and any hosted browser provider you choose. Measure your own workload rather than assuming a fixed cost per intercepted request.

9. Or skip the browser setup

If your goal is to get a screenshot rather than to modify browser traffic, ScreenshotNeo provides a website screenshot API and MCP server. It accepts a URL in one GET request and returns PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation.

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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo and get 1,000 free screenshots a month with no card.

10. FAQ

Does continuing a request replay it?

continue() resolves the intercepted request so it can proceed with any supplied overrides. It is not a replay mechanism by itself.

Can I change a request after it has already been sent?

No. Interception lets you resolve the paused request before it proceeds. Enable interception and register the handler before the request you want to modify.

Do I have to provide all four override fields?

No. The override fields are optional. Supply only the ones you need to change.

Can I use overrides and also block requests?

Yes. Your handler can choose among continuing with overrides, continuing unchanged, aborting, or responding, but each intercepted request must be resolved once under the handling model you use.