How Lightpanda Intercepts Network Requests Using the CDP Fetch Domain
Lightpanda implements network request interception by registering CDP notifications in fetchEnable, pausing HTTP transfers before they reach libcurl, storing them in an InterceptState hash-map, and exposing control commands like Fetch.continueRequest and Fetch.fulfillRequest to modify or synthesize responses.
Lightpanda is a lightweight headless browser written in Zig that implements the Chrome DevTools Protocol (CDP) for automation and debugging. According to the lightpanda-io/browser source code, the browser intercepts network requests using the CDP Fetch domain, which allows clients to pause, modify, or mock HTTP traffic before it reaches the underlying libcurl client.
Enabling Request Interception with Fetch.enable
Interception begins when a CDP client sends the Fetch.enable command. In src/cdp/cdp.zig, the BrowserContext.fetchEnable method registers two notification handlers: http_request_intercept for standard requests and optionally http_request_auth_required for authentication challenges.
// src/cdp/cdp.zig – BrowserContext.fetchEnable
pub fn fetchEnable(self: *Self, authRequests: bool) !void {
try self.notification.register(.http_request_intercept, self, onHttpRequestIntercept);
if (authRequests) {
try self.notification.register(.http_request_auth_required, self, onHttpRequestAuthRequired);
}
}
The authRequests boolean determines whether the browser should pause on HTTP 401/407 challenges, allowing the client to provide credentials via Fetch.continueWithAuth.
Capturing and Pausing Outgoing Requests
Once enabled, every outgoing HTTP request flows through HttpClient.processRequest in src/browser/HttpClient.zig. Before the request reaches libcurl, the system dispatches an .http_request_intercept notification and checks a wait_for_interception flag.
// src/browser/HttpClient.zig – processRequest
transfer.req.notification.dispatch(.http_request_intercept, &.{ .transfer = transfer, .wait_for_interception = &wait_for_interception });
if (!wait_for_interception) { // not intercepted → normal flow
return self.process(transfer);
}
// otherwise the request is paused and stored in InterceptState
If wait_for_interception remains true, the transfer pauses and awaits further instructions from the CDP client.
Storing Paused Requests in InterceptState
Paused transfers are stored in BrowserContext.intercept_state, a hash-map that associates internal requestId values with *HttpClient.Transfer pointers. This storage occurs in src/cdp/domains/fetch.zig.
// src/cdp/domains/fetch.zig – requestIntercept
try bc.intercept_state.put(transfer);
The InterceptState structure guarantees that each paused request can be looked up later by its CDP-generated identifier, enabling reliable continuation or cancellation.
Emitting Fetch.requestPaused Events
After storing the transfer, Lightpanda emits the Fetch.requestPaused event to the CDP client. The payload includes the request URL, method, headers, resource type (Script, XHR, Document, Fetch), and network ID.
// src/cdp/domains/fetch.zig – requestIntercept
try bc.cdp.sendEvent("Fetch.requestPaused", .{
.requestId = &id.toInterceptId(transfer.id),
.frameId = &id.toFrameId(transfer.req.frame_id),
.request = network.TransferAsRequestWriter.init(transfer),
.resourceType = switch (transfer.req.resource_type) {
.script => "Script",
.xhr => "XHR",
.document => "Document",
.fetch => "Fetch",
},
.networkId = &id.toRequestId(transfer.id),
}, .{ .session_id = session_id });
At this point, the request remains paused until the client issues a follow-up command.
Handling Client Commands
The processMessage function in src/cdp/domains/fetch.zig parses incoming CDP commands and dispatches them to the appropriate handler. Lightpanda supports four primary actions for intercepted requests.
// src/cdp/domains/fetch.zig – processMessage
const action = std.meta.stringToEnum(enum {
disable, enable, continueRequest, failRequest,
fulfillRequest, continueWithAuth,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
.disable => return disable(cmd),
.enable => return enable(cmd),
.continueRequest => return continueRequest(cmd),
.continueWithAuth => return continueWithAuth(cmd),
.failRequest => return failRequest(cmd),
.fulfillRequest => return fulfillRequest(cmd),
}
Continuing with Fetch.continueRequest
The continueRequest handler retrieves the stored transfer, applies optional modifications (URL, method, headers, or body), and resumes execution via HttpClient.continueTransfer.
// src/cdp/domains/fetch.zig → continueRequest
const request_id = try idFromRequestId(params.requestId);
const transfer = intercept_state.remove(request_id) orelse return error.RequestNotFound;
// apply optional modifications (url, method, headers, postData) …
try bc.cdp.browser.http_client.continueTransfer(transfer);
If the client provides a headers array, Lightpanda replaces the existing request headers before continuing.
Providing Synthetic Responses with Fetch.fulfillRequest
To mock a response without hitting the network, the client sends Fetch.fulfillRequest. Lightpanda decodes the optional base64-encoded body and creates a synthetic response via HttpClient.fulfillTransfer.
// src/cdp/domains/fetch.zig → fulfillRequest
try bc.cdp.browser.http_client.fulfillTransfer(transfer,
params.responseCode,
params.responseHeaders orelse &.{},
body);
This command immediately resolves the pending request with the provided status code and headers, bypassing libcurl entirely.
Aborting Requests with Fetch.failRequest
To block a request, the client invokes Fetch.failRequest with an error reason such as BlockedByClient. The handler removes the transfer from InterceptState and calls HttpClient.abortTransfer.
// src/cdp/domains/fetch.zig → failRequest
const transfer = intercept_state.remove(request_id) orelse return error.RequestNotFound;
defer bc.cdp.browser.http_client.abortTransfer(transfer);
This decrements the intercepted counter and releases associated resources.
Handling Authentication with Fetch.continueWithAuth
For requests paused due to authentication challenges, continueWithAuth updates credentials and retries the transfer. If the response is not ProvideCredentials, the challenge is aborted; otherwise, the transfer resets with new credentials.
// src/cdp/domains/fetch.zig → continueWithAuth
if (params.authChallengeResponse.response != .ProvideCredentials) {
transfer.abortAuthChallenge();
return cmd.sendResult(null, .{});
}
// set new “user:pass”, reset, and retry
transfer.updateCredentials(...);
transfer.reset();
try bc.cdp.browser.http_client.continueTransfer(transfer);
Practical CDP Command Examples
Enable request interception for all URLs and authentication challenges:
{
"id": 1,
"method": "Fetch.enable",
"params": {
"patterns": [{ "urlPattern": "*", "requestStage": "Request" }],
"handleAuthRequests": true
}
}
Lightpanda responds with a paused request event:
{
"method": "Fetch.requestPaused",
"params": {
"requestId": "INT-42",
"frameId": "FRM-1",
"request": {
"url": "https://example.com/api/data",
"method": "GET",
"headers": [{ "name":"User-Agent", "value":"Lightpanda/1.0" }],
"postData": null,
"hasPostData": false,
"mixedContentType": "none",
"initialPriority": "High"
},
"resourceType": "Fetch",
"networkId": "REQ-99"
}
}
Continue the request unchanged:
{
"id": 2,
"method": "Fetch.continueRequest",
"params": {
"requestId": "INT-42",
"interceptResponse": false
}
}
Modify the request by injecting a custom header:
{
"id": 3,
"method": "Fetch.continueRequest",
"params": {
"requestId": "INT-42",
"headers": [
{ "name": "X-Debug", "value": "true" }
]
}
}
Fulfill the request with a synthetic JSON response:
{
"id": 4,
"method": "Fetch.fulfillRequest",
"params": {
"requestId": "INT-42",
"responseCode": 200,
"responseHeaders": [{ "name":"Content-Type", "value":"application/json" }],
"body": "eyJtZXNzYWdlIjoiSGVsbG8ifQ=="
}
}
Abort the request with an error reason:
{
"id": 5,
"method": "Fetch.failRequest",
"params": {
"requestId": "INT-42",
"errorReason": "BlockedByClient"
}
}
Summary
- Lightpanda implements the CDP Fetch domain in
src/cdp/domains/fetch.zigto intercept network requests before they reach libcurl. - The
Fetch.enablecommand registershttp_request_interceptnotifications that pause transfers inHttpClient.processRequest. - Paused requests are stored in
BrowserContext.intercept_state, a hash-map keyed by CDP request IDs, ensuring reliable lookup for subsequent commands. - Clients control interception via
Fetch.continueRequest,Fetch.fulfillRequest,Fetch.failRequest, andFetch.continueWithAuth, each mapping to specificHttpClientmethods likecontinueTransfer,fulfillTransfer, andabortTransfer. - Resource types (Script, XHR, Document, Fetch) are mapped explicitly when emitting
Fetch.requestPausedevents, providing full context to the CDP client.
Frequently Asked Questions
What CDP domain does Lightpanda use for network request interception?
Lightpanda uses the Fetch domain of the Chrome DevTools Protocol. This domain provides commands like Fetch.enable and events like Fetch.requestPaused that allow clients to pause, modify, or mock HTTP requests before they are sent to the network layer.
How does Lightpanda store intercepted requests while waiting for client instructions?
When a request is intercepted, Lightpanda stores the HttpClient.Transfer object in BrowserContext.intercept_state, which is a hash-map that associates CDP requestId strings with transfer pointers. This InterceptState structure lives in src/cdp/domains/fetch.zig and ensures that each paused request can be retrieved and resumed reliably when the client sends a continue, fulfill, or fail command.
Can I modify request headers when continuing a request in Lightpanda?
Yes. When calling Fetch.continueRequest, you can provide a headers array in the parameters. The continueRequest handler in src/cdp/domains/fetch.zig applies these modifications to the stored transfer before calling HttpClient.continueTransfer, allowing you to inject, replace, or remove headers before the request proceeds to libcurl.
What HTTP client does Lightpanda use under the hood for intercepted requests?
Lightpanda uses libcurl wrapped by the HttpClient struct defined in src/browser/HttpClient.zig. The Fetch domain handlers interact with this client through methods like continueTransfer, fulfillTransfer, and abortTransfer, keeping the CDP logic completely separate from the low-level HTTP implementation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →