# How Lightpanda Intercepts Network Requests Using the CDP Fetch Domain

> Learn how Lightpanda intercepts network requests using the CDP Fetch domain. It pauses HTTP transfers, stores them, and allows modification or synthesis of responses for greater control.

- Repository: [Lightpanda/browser](https://github.com/lightpanda-io/browser)
- Tags: how-to-guide
- Published: 2026-03-14

---

**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.

```zig
// 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.

```zig
// 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`.

```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.

```zig
// 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.

```zig
// 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`.

```zig
// 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`.

```zig
// 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`.

```zig
// 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.

```zig
// 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:

```json
{
  "id": 1,
  "method": "Fetch.enable",
  "params": {
    "patterns": [{ "urlPattern": "*", "requestStage": "Request" }],
    "handleAuthRequests": true
  }
}

```

Lightpanda responds with a paused request event:

```json
{
  "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:

```json
{
  "id": 2,
  "method": "Fetch.continueRequest",
  "params": {
    "requestId": "INT-42",
    "interceptResponse": false
  }
}

```

Modify the request by injecting a custom header:

```json
{
  "id": 3,
  "method": "Fetch.continueRequest",
  "params": {
    "requestId": "INT-42",
    "headers": [
      { "name": "X-Debug", "value": "true" }
    ]
  }
}

```

Fulfill the request with a synthetic JSON response:

```json
{
  "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:

```json
{
  "id": 5,
  "method": "Fetch.failRequest",
  "params": {
    "requestId": "INT-42",
    "errorReason": "BlockedByClient"
  }
}

```

## Summary

- **Lightpanda** implements the CDP **Fetch** domain in `src/cdp/domains/fetch.zig` to intercept network requests before they reach libcurl.
- The `Fetch.enable` command registers `http_request_intercept` notifications that pause transfers in `HttpClient.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`, and `Fetch.continueWithAuth`, each mapping to specific `HttpClient` methods like `continueTransfer`, `fulfillTransfer`, and `abortTransfer`.
- Resource types (Script, XHR, Document, Fetch) are mapped explicitly when emitting `Fetch.requestPaused` events, 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.