Free tools Windows power users keep installed
One-click scans. No signup required.
Use Chrome DevTools Protocol (CDP) when you need dependable visibility into non-HTTP traffic. Puppeteer’s page.on('request') and setRequestInterception APIs are convenient for ordinary allow, block, and mock decisions, but their documented object is an HTTPRequest. For file:, data:, other schemes, and WebSocket frames, attach a CDP Network session. When a matching request must be paused and continued, failed, or replaced, use CDP Fetch.
Choose the interception layer first
The right API depends on whether you want a simple decision, broad observation, or a paused request that you can replace.
| Goal | Recommended layer | What it provides |
|---|---|---|
| Block images, fonts, media, or selected URLs | page.setRequestInterception and page.on('request') |
Short handlers that call continue(), abort(), or respond(). |
Observe file:, data:, other schemes, and network lifecycle events |
CDP Network |
Protocol events for a wider set of requests and responses. |
| Pause, fail, or synthesize a matching response | CDP Fetch |
Explicit paused-request state with continueRequest, failRequest, and fulfillRequest. |
| Inspect WebSocket messages | CDP Network WebSocket events |
Handshake and frame events, including sent and received payloads. |
| Restrict broad browser network access | Puppeteer ConnectOptions.allowlist/blocklist |
An experimental network-service guardrail, not a complete sandbox. |
Browser and Chromium versions can change which schemes reach a particular layer. Test the exact browser build you deploy, especially when you need to replace a non-HTTP resource rather than merely observe it.
How Puppeteer request interception behaves
In Puppeteer 25.12.0, enabling interception changes the default control flow: “Once request interception is enabled, every request will stall unless it’s continued, responded or aborted.” Enable it before goto, reload, a click, worker creation, or any action that causes traffic.
#1 Best Overall
Basic high-level filtering
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', request => {
// This check must happen immediately before resolving the request.
if (request.isInterceptResolutionHandled()) return;
const url = request.url();
if (url.startsWith('file:') || url.startsWith('data:')) {
request.abort();
return;
}
request.continue();
});
await page.goto('https://example.com', {waitUntil: 'networkidle2'});
await browser.close();
This is suitable for ordinary resource filtering, but do not assume every browser-internal or non-network fetch will produce a high-level HTTPRequest event. Puppeteer’s API describes HTTPRequest as an HTTP request sent by a page; CDP is the safer observation layer for broader scheme coverage.
Preventing handler races
Multiple listeners can compete to resolve one request. Check isInterceptResolutionHandled() immediately before abort, continue, or respond, and check again after every await. The check and the resolution call should remain in the same synchronous block. Puppeteer’s cooperative mode can assign numeric priorities; the highest priority wins, and ties are ordered abort, respond, then continue.
page.on('request', async request => {
if (request.isInterceptResolutionHandled()) return;
const shouldMock = request.url().endsWith('/config.json');
if (!shouldMock) {
request.continue();
return;
}
// If asynchronous work is added, re-check before resolving.
const body = JSON.stringify({feature: 'test'});
if (request.isInterceptResolutionHandled()) return;
request.respond({
status: 200,
contentType: 'application/json',
body
});
});
Observe file, data, and other non-HTTP requests with CDP Network
Chrome’s CDP Network domain exposes information about HTTP, file, data, and other request types. Create a CDP session, enable the domain before navigation, and inspect Network.requestWillBeSent.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
const client = await page.createCDPSession();
await client.send('Network.enable');
client.on('Network.requestWillBeSent', event => {
const {url} = event.request;
if (!/^https?:/i.test(url)) {
console.log('non-HTTP request', {
type: event.type,
url,
requestId: event.requestId
});
}
});
await page.goto('file:///tmp/example.html', {waitUntil: 'load'});
await browser.close();
This listener observes traffic; it does not itself pause or replace it. Keep the session attached for the entire operation that may create the request, including frames and workers relevant to your test.
What observation can and cannot prove
- A
Network.requestWillBeSentevent proves that Chrome exposed the request to the CDP Network domain. - No high-level
requestevent does not prove that the page made no non-HTTP request; it may simply be outside Puppeteer’sHTTPRequestabstraction. - Seeing a URL does not give you an HTTP response to fulfill. Schemes without an HTTP response model should generally be observed rather than replaced.
Pause and mock requests with CDP Fetch
Use CDP Fetch when a matching request must stop until your code chooses an outcome. Fetch.enable accepts URL patterns and resource-type filters. Every matching Fetch.requestPaused event remains paused until you call Fetch.continueRequest, Fetch.failRequest, or Fetch.fulfillRequest. You can intercept at the request stage or response stage.
Minimal response fulfillment
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
const client = await page.createCDPSession();
await client.send('Fetch.enable', {
patterns: [{
urlPattern: 'https://example.test/*',
requestStage: 'Request'
}]
});
client.on('Fetch.requestPaused', async ({requestId, request}) => {
try {
if (request.url.startsWith('https://example.test/')) {
const body = Buffer.from(JSON.stringify({ok: true})).toString('base64');
await client.send('Fetch.fulfillRequest', {
requestId,
responseCode: 200,
responseHeaders: [
{name: 'content-type', value: 'application/json'}
],
body
});
} else {
await client.send('Fetch.continueRequest', {requestId});
}
} catch (error) {
console.error('Could not resolve paused request', request.url, error);
// A request that remains paused can hang the page. If it is still valid,
// attempt to let it continue.
try {
await client.send('Fetch.continueRequest', {requestId});
} catch {}
}
});
await page.goto('https://example.com', {waitUntil: 'networkidle2'});
await browser.close();
Fetch.fulfillRequest models an HTTP-shaped response. It is appropriate for HTTP endpoints such as the example above. For file:, data:, or another scheme that has no HTTP response semantics, prefer CDP Network observation and verify the exact Chromium behavior before promising replacement.
Failing or continuing instead
client.on('Fetch.requestPaused', async ({requestId, request}) => {
if (request.url.includes('/telemetry')) {
await client.send('Fetch.failRequest', {
requestId,
errorReason: 'BlockedByClient'
});
return;
}
await client.send('Fetch.continueRequest', {requestId});
});
Always include a fallback continuation for requests you do not intend to modify. Also disable Fetch when the job is over if the CDP session will be reused:
await client.send('Fetch.disable');
Intercepting WebSocket traffic
A WebSocket has two distinct interception problems: the opening handshake and the frames sent after the connection is established. Chrome’s webRequest support covers the handshake, not individual messages. CDP Network exposes both handshake events and frame-level events.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Rank #3
const client = await page.createCDPSession();
await client.send('Network.enable');
client.on('Network.webSocketWillSendHandshakeRequest', event => {
console.log('WS handshake', event.request.url);
});
client.on('Network.webSocketFrameSent', event => {
console.log('WS sent frame', event.response.payloadData);
});
client.on('Network.webSocketFrameReceived', event => {
console.log('WS received frame', event.response.payloadData);
});
You may make a handshake decision through the interception layer Chrome exposes for that request. Do not claim that page.on('request') can rewrite arbitrary post-connect WebSocket messages; inspect those with the frame events instead.
A reliable setup sequence
- Create the page and, when needed, a CDP session.
- Register Puppeteer request listeners or CDP event listeners.
- Enable
NetworkorFetchbefore navigation or the action that generates traffic. - For every intercepted request, resolve exactly once: continue, fail, fulfill, abort, or respond.
- Navigate or perform the user action.
- Collect logs and errors, then disable Fetch and close the browser.
Registering too late is a common source of missing events. Registering multiple competing listeners is a common source of “already handled” errors and hangs.
Troubleshooting non-HTTP interception
The page hangs after interception is enabled
Cause: at least one request was left unresolved. Add request.continue() to the high-level fallback and Fetch.continueRequest to the CDP fallback. Log every Fetch.requestPaused event and ensure exceptions in the handler cannot skip resolution.
page.on('request') never sees a file: or data: URL
Cause: the request is outside Puppeteer’s HTTPRequest abstraction or the browser did not expose it at that layer. Enable CDP Network before the triggering action and inspect Network.requestWillBeSent. Browser-version differences are expected.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
“Request is already handled” or duplicate-resolution errors appear
Cause: more than one handler raced, often because one handler awaited a promise. Check isInterceptResolutionHandled() immediately before each resolution and again after every await; consolidate handlers when possible.
Fetch interception catches more traffic than intended
Cause: a broad URL pattern or missing resource-type filter. Narrow urlPattern, add the appropriate resource type, and log the URL before choosing an action. Keep an explicit continuation for non-matches.
Fulfillment fails for a non-HTTP scheme
Cause: Fetch.fulfillRequest represents an HTTP response and the target scheme may not have one. Use Network events for observation, or validate the behavior against the Chromium version you actually run before designing a replacement.
WebSocket frames are missing
Cause: only the handshake was observed, or Network was enabled after the socket opened. Enable Network first and subscribe to Network.webSocketFrameSent and Network.webSocketFrameReceived.
Recommended Free Tools
Performance, reliability, and security considerations
- Interception adds handler work to every matched request; keep synchronous decisions cheap and avoid unnecessary awaits.
- Fetch pauses matching requests, so slow mocks directly increase page latency and can trigger application timeouts.
- Capture request IDs, URLs, and resolution errors in test logs so a failed navigation can be diagnosed without guessing which request stalled.
- Use narrow patterns rather than pausing all traffic. Broad interception increases overhead and makes accidental hangs more likely.
- The experimental
allowlist/blocklistoptions are guardrails. Puppeteer documents limits because some browser network access and web features may bypass the network service; do not treat them as a complete sandbox. - Do not log authorization headers, cookies, or sensitive WebSocket payloads in shared CI output.
Or skip the browser setup
If your actual goal is a clean screenshot or PDF rather than traffic analysis, ScreenshotNeo removes the browser orchestration. It accepts a URL and returns PNG, JPEG, WebP, or PDF; its API can also wait, block resources, set headers and cookies, run custom JavaScript, and capture a selected element.
One call is enough:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the complete parameter list and response behavior in the ScreenshotNeo documentation. The same request in 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)
And in 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}`);
Before capture, ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and each response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server provides 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 without a card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account to try 1,000 screenshots a month with no card.
Frequently Asked Questions
Can Puppeteer intercept a WebSocket’s messages with the request event?
No. The request event concerns the opening request abstraction; inspect post-connect traffic with CDP Network WebSocket frame events.
Should I use CDP Network or CDP Fetch for a file URL?
Use CDP Network to observe it. Fetch fulfillment is HTTP-shaped, so replacement semantics for non-HTTP schemes must be verified against the Chromium version you run.
Why must interception be enabled before navigation?
Requests can start as soon as navigation or another action begins. Enabling the listener afterward cannot recover events that already occurred.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




