ScreenshotNeo

BlogHow-to

How to Capture All Request Headers with Puppeteer Request Interception

Capture, inspect, override, and troubleshoot request headers in Puppeteer without stalling navigation or losing redirected requests.

By the ScreenshotNeo team30 September 20269 min read

How to Capture All Request Headers with Puppeteer Request Interception

Direct answer: enable request interception before navigation, listen for the request event, read request.headers(), and resolve every intercepted request. A passive capture handler looks like this:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();

await page.setRequestInterception(true);

page.on('request', request => {
  const headers = request.headers();
  console.log({
    method: request.method(),
    url: request.url(),
    headers
  });

  request.continue();
});

await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await browser.close();

headers() returns the headers associated with that request as an object. Puppeteer documents the returned header names as lower-case, so compare names case-insensitively. Interception pauses requests until you continue, respond, abort, or otherwise complete them; forgetting request.continue() is the most common reason a page hangs. See the HTTPRequest.headers() reference and Puppeteer’s request interception guide.

1. What “all request headers” means in Puppeteer

Puppeteer exposes request headers through the HTTPRequest object delivered to the request event. The method returns a JavaScript object whose keys are lower-case header names and whose values are strings. This is the browser’s request representation, not a raw packet capture. It should not be treated as a byte-for-byte transcript of the wire, a guarantee of original casing, or proof that every transport-level detail is visible.

Puppeteer observes each page request before it continues.
Puppeteer observes each page request before it continues.

A page can make many requests during one navigation: the document, stylesheets, scripts, images, fonts, XHR or fetch calls, preloads, analytics, service-worker traffic, redirects, and requests generated after the initial load. The listener runs for each request Puppeteer reports. If you need a complete run record, also subscribe to requestfailed and requestfinished, and remember that a redirect creates another request lifecycle.

Request headers versus response headers

Use request.headers() for outbound request headers. To inspect inbound response headers, read the response object’s headers() method from a response event. Response header names are also lower-case. Duplicate response values are combined into a comma-separated string except Set-Cookie, which is separated by newlines. Do not use response headers as a substitute for request headers.

2. A production-ready capture script

The following script records every request, records completion or failure, handles redirects naturally, and writes newline-delimited JSON. It keeps the interception callback synchronous, which reduces coordination problems with other listeners.

import puppeteer from 'puppeteer';
import { createWriteStream } from 'node:fs';

const target = process.argv[2] ?? 'https://example.com';
const output = createWriteStream('requests.ndjson', { flags: 'w' });
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();

function write(record) {
  output.write(JSON.stringify({
    time: new Date().toISOString(),
    ...record
  }) + '\\n');
}

await page.setRequestInterception(true);

page.on('request', request => {
  const record = {
    type: 'request',
    id: request._requestId,
    method: request.method(),
    url: request.url(),
    resourceType: request.resourceType(),
    headers: request.headers(),
    isNavigationRequest: request.isNavigationRequest()
  };

  write(record);

  // Every request must be resolved.
  if (!request.isInterceptResolutionHandled()) {
    request.continue();
  }
});

page.on('requestfinished', request => {
  write({
    type: 'requestfinished',
    method: request.method(),
    url: request.url(),
    resourceType: request.resourceType()
  });
});

page.on('requestfailed', request => {
  write({
    type: 'requestfailed',
    method: request.method(),
    url: request.url(),
    resourceType: request.resourceType(),
    failure: request.failure()
  });
});

page.on('response', response => {
  write({
    type: 'response',
    status: response.status(),
    url: response.url(),
    headers: response.headers()
  });
});

try {
  await page.goto(target, { waitUntil: 'networkidle2', timeout: 60_000 });
} finally {
  output.end();
  await browser.close();
}

Install and run it with:

npm install puppeteer
node capture-headers.js https://example.com

The private _requestId property is not a stable public API. Remove it if you need a script that remains portable across Puppeteer versions. A safer correlation key is your own incrementing counter, combined with method and URL, although URLs can repeat.

3. Capturing only the requests you need

Capturing every resource is useful for audits, but filtering makes logs smaller and reduces work. You can inspect all requests while storing only selected resource types, hosts, or header names.

const interestingHosts = new Set(['api.example.com', 'example.com']);

page.on('request', request => {
  const url = new URL(request.url());
  const headers = request.headers();

  if (interestingHosts.has(url.hostname)) {
    console.log({
      method: request.method(),
      url: request.url(),
      authorizationPresent: 'authorization' in headers,
      contentType: headers['content-type'] ?? null,
      headers
    });
  }

  if (!request.isInterceptResolutionHandled()) {
    request.continue();
  }
});

Never print secrets casually. Authorization tokens, cookies, signed URLs, and custom credentials commonly appear in headers. Redact values before writing logs that leave the machine:

function redact(headers) {
  const secretNames = new Set([
    'authorization', 'proxy-authorization', 'cookie', 'set-cookie', 'x-api-key'
  ]);

  return Object.fromEntries(
    Object.entries(headers).map(([name, value]) => [
      name,
      secretNames.has(name.toLowerCase()) ? '[REDACTED]' : value
    ])
  );
}

4. Adding or overriding headers

Page-wide additions with setExtraHTTPHeaders()

If the goal is simply to add the same headers to requests initiated by a page, use page.setExtraHTTPHeaders(). Puppeteer says header names are lower-cased, values must be strings, and outgoing header order is not guaranteed.

await page.setExtraHTTPHeaders({
  'x-correlation-id': 'run-2026-09-30-001',
  'accept-language': 'en-US,en;q=0.9'
});

await page.goto('https://example.com');

This API is the clearest choice for a common page-wide value. It does not give you a per-request decision point.

Per-request overrides with continue()

When only selected requests should change, start with the existing header object and pass an override to request.continue({ headers }).

await page.setRequestInterception(true);

page.on('request', request => {
  const headers = {
    ...request.headers(),
    'x-debug-capture': 'true'
  };

  if (!request.isInterceptResolutionHandled()) {
    request.continue({ headers });
  }
});

Use this pattern when preserving Puppeteer’s listed values matters. Header names should be treated case-insensitively; because Puppeteer returns lower-case names, write overrides in lower-case too.

5. Safe interception when multiple handlers exist

Interception handlers can conflict. Another listener, plugin, or package may resolve a request before your listener does. Puppeteer provides request.isInterceptResolutionHandled() for this case.

Check immediately before resolving. If your handler performs asynchronous work, check again after the await; the result may have changed while your code was paused.

page.on('request', async request => {
  const headers = request.headers();
  await saveHeadersSomewhere(headers);

  // The request could have been resolved during the await.
  if (request.isInterceptResolutionHandled()) return;
  request.continue();
});

For the lowest risk, collect the data synchronously and defer expensive logging to a queue. Puppeteer’s current guidance also describes Cooperative Intercept Mode: it is active only when all handlers provide numeric priorities. A handler that resolves without a priority uses legacy immediate resolution, so do not assume cooperative behavior when third-party listeners are present.

6. Redirects, failures, cache, and navigation details

  • Redirects: inspect each request event. The redirected URL can have a different host, method, and header set.
  • Failures: a requestfailed event tells you the request did not complete. Record request.failure() and keep the browser alive long enough to flush logs.
  • Successful completion: requestfinished indicates the request completed from Puppeteer’s perspective.
  • Cached resources: some requests can complete from browser cache. They still may have an interception lifecycle, but do not assume a network packet was sent.
  • Service workers: page activity can be fulfilled by a service worker, which changes what appears as a network request. If your audit needs service-worker behavior, test with the same browser context and policies used in production.
  • Non-page traffic: page.on('request') covers requests issued by that page. Other pages, popups, and browser-level traffic need their own listeners or a broader browser instrumentation approach.

7. Troubleshooting common errors

Symptom Cause Fix
Navigation never finishes Interception was enabled but a request was not resolved. Call continue(), abort(), or respond() on every path, including error paths.
“Request is already handled” Another listener resolved the request, often while your code awaited. Check isInterceptResolutionHandled() immediately before resolving and again after every await.
Expected Authorization key is missing Puppeteer returns header names in lower-case. Read headers.authorization and normalize names for comparisons.
Only the first document appears The listener was attached after navigation or filtering excluded subresources. Enable interception and attach listeners before goto(); inspect resourceType().
Header order differs Browser and Puppeteer do not promise outgoing order. Compare names and values, not serialized order. Do not use order as a protocol signal.
Logs contain credentials Cookies, bearer tokens, or API keys were recorded verbatim. Redact sensitive names before persistence and restrict log access.
Redirected request was missed The code tracked only the initial URL. Handle every request event and store redirect relationships separately if needed.
Headers differ between local and CI Different browser versions, contexts, user agents, extensions, or proxy settings. Pin the relevant runtime, record environment metadata, and compare the same context configuration.

8. Performance and reliability practices

  1. Keep the event path short. Read and normalize headers, enqueue a record, and continue. Synchronous file writes or network calls inside the handler delay every request.
  2. Use bounded logs. Large pages can produce thousands of records. Filter by hostname or resource type, rotate files, and cap retained body data. Header capture alone usually needs far less storage than response capture.
  3. Set explicit timeouts. Use a navigation timeout and an outer job deadline. Always close the browser in a finally block.
  4. Measure completion. Count requests seen, finished, failed, and still pending when the deadline expires. This distinguishes a quiet page from an incomplete capture.
  5. Test with redirects and failures. A script that works on a static HTML page can still lose data on authentication redirects, blocked resources, or flaky APIs.
  6. Avoid accidental mutation. If you only inspect headers, call plain continue(). If you override, copy the original object and change only the intended keys.

9. cURL and Python perspective

Puppeteer interception is browser-specific. cURL and Python HTTP clients can inspect headers on requests they themselves send, but they do not automatically observe every subresource, redirect decision, service-worker interaction, or browser-generated request from a rendered page.

curl -v -H 'X-Debug-Capture: true' https://example.com
import requests

response = requests.get(
    'https://example.com',
    headers={'X-Debug-Capture': 'true'},
    timeout=30,
)
print(response.request.headers)
print(response.headers)

Use these approaches for direct HTTP calls. Use Puppeteer when the question is “what did this rendered browser page request?”

10. Or skip the browser setup

If your end goal is a clean screenshot rather than a network audit, ScreenshotNeo provides a website screenshot API and MCP server. It handles the browser workflow with one GET request and returns PNG, JPEG, WebP, or PDF. Before capture it accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports the result with X-Page-Verdict and X-Billed headers.

A hosted screenshot service can remove common overlays before capture.
A hosted screenshot service can remove common overlays before capture.

See the ScreenshotNeo API documentation for all options, including full-page lazy-image capture, CSS-element capture, dark mode, device presets, custom viewports, retina scale, PDF settings, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous jobs, webhooks, bulk capture, usage data, and the OpenAPI specification.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

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)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo includes an MCP server with take_screenshot, get_page_info, and capture_pdf tools 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 shots. Create a free ScreenshotNeo account.

11. Cost and operational considerations

Local Puppeteer cost is primarily your compute, browser startup time, bandwidth, storage, and engineering maintenance. Interception itself does not make a request cheaper; it adds a handler to every observed request, so avoid expensive per-request work.

For a hosted screenshot workflow, ScreenshotNeo bills only clean shots. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. Its plans are Free (1,000 per month), Starter ($5 for 3,000), Growth ($15 for 15,000), Pro ($39 for 60,000), Scale ($99 for 250,000), and Business ($249 for 1,000,000); yearly billing gives two months free. Every feature is included on every plan.

12. Checklist

  • Enable interception before goto().
  • Read request.headers() inside the request listener.
  • Expect lower-case names.
  • Resolve every request.
  • Guard against competing handlers.
  • Record redirects, failures, and completions.
  • Redact credentials.
  • Use setExtraHTTPHeaders() for page-wide additions.
  • Use continue({ headers }) for per-request overrides.
  • Close the browser and flush logs on errors.

13. FAQ

Does request.headers() include every header sent on the wire?

No. It returns Puppeteer’s documented header object with lower-case names. Treat it as the browser’s associated request headers, not a raw packet capture.

Can I capture response headers too?

Yes. Listen for response and call response.headers(). Keep request and response records separate because they describe opposite directions.

Should I use interception just to add one header?

Usually use page.setExtraHTTPHeaders() for a value that applies to every page request. Use interception when the decision depends on URL, method, resource type, or existing headers.

Why are header names lower-case?

Puppeteer documents lower-case names for request headers. HTTP field names are case-insensitive, so normalize comparisons instead of relying on capitalization.

Can Puppeteer capture requests from a popup?

Attach interception and listeners to the relevant page or popup. A listener on one page does not automatically describe every other page in the browser.