Skip to content

How to Intercept and Modify Network Requests in Lightpanda Browser

Purpose

I needed to intercept network requests in Lightpanda to mock API responses for testing. When I tried to find documentation on how to do this, I couldn’t find any clear examples. This post shows exactly how I got request interception working using the CDP Fetch domain.

What I Was Trying to Do

I was building a test suite for a web application and needed to:

  1. Mock API responses without modifying the backend
  2. Block tracking scripts and ads
  3. Add authentication headers dynamically

I knew Chrome DevTools Protocol (CDP) had a Fetch domain for this, but Lightpanda is a newer browser and I wasn’t sure if it was supported.

Environment

  • Lightpanda 0.1.0
  • Node.js 20.x
  • Puppeteer for CDP connection

First Attempt - Using Network Domain

I started by trying the Network domain, which I was familiar with from Puppeteer:

test-network.js
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.connect({
browserWSEndpoint: "ws://127.0.0.1:9222",
});
const page = await browser.newPage();
// Enable Network domain to see requests
const client = await page.target().createCDPSession();
await client.send('Network.enable');
client.on('Network.requestWillBeSent', (params) => {
console.log('Request:', params.request.url);
});
await page.goto('https://example.com');

This worked for monitoring requests, but I couldn’t intercept or modify them. The Network domain only lets you observe, not modify. I needed something different.

Discovery - Fetch Domain

I dug into Lightpanda’s source code and found it does support the Fetch domain. The Fetch domain is specifically designed for request interception.

From src/cdp/domains/fetch.zig:

  • InterceptState manages request interception
  • Request interception via notification system
  • Auth request handling support

The Fetch domain intercepts requests before they’re sent to the server.

How to Enable Request Interception

Step 1: Connect to Lightpanda

connect.mjs
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.connect({
browserWSEndpoint: "ws://127.0.0.1:9222",
});
const page = await browser.newPage();
const client = await page.target().createCDPSession();

Step 2: Enable the Fetch Domain

enable-fetch.mjs
// Enable Fetch with request patterns
await client.send('Fetch.enable', {
patterns: [
{
urlPattern: '*',
requestStage: 'Request'
}
]
});

The requestStage: 'Request' tells Lightpanda to intercept before the request is sent.

Step 3: Handle requestPaused Events

When a request matches your pattern, Lightpanda pauses it and fires a requestPaused event:

handle-pause.mjs
client.on('Fetch.requestPaused', async ({ requestId, request }) => {
console.log('Intercepted:', request.url);
// Option 1: Continue with original request
await client.send('Fetch.continueRequest', { requestId });
});

Practical Use Cases

Mocking API Responses

This is what I needed originally - return fake data instead of calling the real API:

mock-api.mjs
client.on('Fetch.requestPaused', async ({ requestId, request }) => {
// Intercept specific API calls
if (request.url.includes('/api/users')) {
// Return mock response instead of hitting the server
await client.send('Fetch.fulfillRequest', {
requestId,
responseCode: 200,
responseHeaders: [
{ name: 'Content-Type', value: 'application/json' }
],
body: Buffer.from(JSON.stringify({
users: [
{ id: 1, name: 'Test User' },
{ id: 2, name: 'Mock User' }
]
})).toString('base64')
});
return;
}
// Let other requests through
await client.send('Fetch.continueRequest', { requestId });
});

Blocking Requests

To block tracking scripts and ads:

block-requests.mjs
const blockedPatterns = [
'analytics.js',
'tracking.js',
'adservice',
'doubleclick.net'
];
client.on('Fetch.requestPaused', async ({ requestId, request }) => {
// Check if URL matches blocked patterns
const shouldBlock = blockedPatterns.some(pattern =>
request.url.includes(pattern)
);
if (shouldBlock) {
console.log('Blocked:', request.url);
await client.send('Fetch.failRequest', {
requestId,
errorReason: 'BlockedByClient'
});
return;
}
// Continue normal requests
await client.send('Fetch.continueRequest', { requestId });
});

Modifying Request Headers

Adding authentication headers dynamically:

modify-headers.mjs
client.on('Fetch.requestPaused', async ({ requestId, request }) => {
// Add auth header to API requests
if (request.url.includes('/api/')) {
const headers = Object.entries(request.headers).map(([name, value]) => ({
name, value
}));
headers.push({ name: 'Authorization', value: 'Bearer my-secret-token' });
await client.send('Fetch.continueRequest', {
requestId,
headers
});
return;
}
// Continue without modification
await client.send('Fetch.continueRequest', { requestId });
});

Complete Example

Here’s a complete working example:

lightpanda-intercept.mjs
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.connect({
browserWSEndpoint: "ws://127.0.0.1:9222",
});
const page = await browser.newPage();
const client = await page.target().createCDPSession();
// Enable Fetch domain with patterns
await client.send('Fetch.enable', {
patterns: [
{ urlPattern: '*', requestStage: 'Request' }
]
});
// Handle requestPaused events
client.on('Fetch.requestPaused', async ({ requestId, request }) => {
console.log(`[${request.method}] ${request.url}`);
// Mock specific API
if (request.url.includes('/api/status')) {
await client.send('Fetch.fulfillRequest', {
requestId,
responseCode: 200,
responseHeaders: [
{ name: 'Content-Type', value: 'application/json' }
],
body: Buffer.from(JSON.stringify({ status: 'ok' })).toString('base64')
});
return;
}
// Block tracking
if (request.url.includes('analytics')) {
await client.send('Fetch.failRequest', {
requestId,
errorReason: 'BlockedByClient'
});
return;
}
// Continue all other requests
await client.send('Fetch.continueRequest', { requestId });
});
await page.goto('https://example.com');
await browser.close();

What Didn’t Work

I tried a few things that failed:

  1. Using Network.requestIntercepted - This method exists in older CDP versions but is deprecated. Lightpanda uses the newer Fetch domain.

  2. Intercepting without patterns - You must specify at least one pattern in Fetch.enable. Passing an empty patterns array does nothing.

  3. Base64 encoding issues - The body parameter in fulfillRequest must be base64 encoded. I initially forgot this and got invalid responses.

Summary

In this post, I showed how to use Lightpanda’s CDP Fetch domain for network interception. The key steps are:

  1. Enable Fetch with Fetch.enable and specify URL patterns
  2. Listen for requestPaused events
  3. Use continueRequest, fulfillRequest, or failRequest to control the request

This enables sophisticated testing scenarios like mocking APIs, blocking resources, and modifying requests on the fly - all without touching the backend server.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments