How Lightpanda Implements Request Interception with Authentication Handling

Lightpanda implements request interception with authentication handling by injecting a pause mechanism into its HTTP client that dispatches events through a notification system to the CDP Fetch domain, allowing remote clients to modify, fulfill, abort, or provide credentials for requests via standard Chrome DevTools Protocol commands.

The Lightpanda browser engine provides a comprehensive request interception system that operates at the CDP (Chrome DevTools Protocol) level. This architecture enables fine-grained control over network traffic, including the ability to handle HTTP authentication challenges programmatically. Understanding how Lightpanda implements request interception with authentication handling requires examining the interaction between its low-level network stack, event notification system, and CDP domain implementations.

Core Architecture of Request Interception and Authentication

HttpClient: The Network Entry Point

In src/browser/HttpClient.zig, the processRequest function serves as the interception gateway. When creating a new Transfer for an outgoing request, the client immediately fires two notification events:

transfer.req.notification.dispatch(.http_request_start, &.{ .transfer = transfer });
var wait_for_interception = false;
transfer.req.notification.dispatch(.http_request_intercept, &.{ .transfer = transfer,
                                                            .wait_for_interception = &wait_for_interception });

The wait_for_interception boolean flag determines whether the request proceeds normally or enters a paused state. If no interceptor is registered, the flag remains false and the request continues via self.process(transfer). When interception is active, the flag sets to true, causing Lightpanda to increment the self.intercepted counter, mark the transfer with ._intercept_state = .pending, and either return early for non-blocking requests or block until the client calls continueTransfer or abortTransfer.

For authentication challenges, the same pattern applies using a distinct notification type dispatched in src/browser/HttpClient.zig (lines 809-818):

transfer.req.notification.dispatch(.http_request_auth_required,
    &.{ .transfer = transfer, .wait_for_interception = &wait_for_interception });

This design allows the network layer to remain agnostic about interception logic while providing clear extension points for authentication handling.

Notification System: Event Dispatch

The Notification type in src/Notification.zig implements a lightweight publish/subscribe pattern that decouples the network stack from CDP handling. Listeners register for specific event types using the notification.register method:

try self.notification.register(.http_request_intercept, self, onHttpRequestIntercept);

When an event dispatches, the system calls every registered listener with the appropriate payload, transporting Transfer references from the network layer to the CDP domain without direct coupling.

CDP Fetch Domain: Protocol Bridge

The src/cdp/domains/fetch.zig file contains the CDP Fetch domain implementation that translates internal network events into standard CDP protocol events. When Fetch.enable is called with handleAuthRequests: true, the domain registers listeners for both request interception and authentication challenges:

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);
    }
}

This registration occurs in src/cdp/cdp.zig (lines 560-665), establishing the connection between network events and protocol responses.

Request Lifecycle and CDP Control Commands

When the network stack signals an interception, the CDP domain creates a paused event via onHttpRequestIntercept. The handler generates a Fetch.requestPaused event containing the request ID, frame ID, resource type, and network ID:

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) { … },
    .networkId    = &id.toRequestId(transfer.id),
}, .{ .session_id = session_id });

The request enters a per-session InterceptState map that correlates request IDs to Transfer instances. The client can then issue specific CDP commands to control the paused request:

  • Fetch.continueRequest: Handler continueRequest (lines 226-269) updates URL, method, headers, or body before calling http_client.continueTransfer.
  • Fetch.failRequest: Handler calls http_client.abortTransfer to abort the request.
  • Fetch.fulfillRequest: Handler fulfillRequest sends a custom response directly to the client without hitting the network.

The continueRequest handler extracts the stored transfer from the InterceptState map and applies modifications before resuming:

const transfer = intercept_state.remove(request_id) orelse return error.RequestNotFound;
…
try bc.cdp.browser.http_client.continueTransfer(transfer);

Handling HTTP Authentication Challenges

When a server issues an HTTP authentication challenge, the network layer dispatches .http_request_auth_required via the same notification mechanism. The CDP domain responds by sending a Fetch.authRequired event containing challenge details including the scheme (basic or digest), realm, and source (server or proxy):

try bc.cdp.sendEvent("Fetch.authRequired", .{
    .requestId = &id.toInterceptId(transfer.id),
    .frameId   = &id.toFrameId(transfer.req.frame_id),
    .request   = network.TransferAsRequestWriter.init(transfer),
    .authChallenge = .{
        .origin = "",
        .source = if (challenge.source) |s| (if (s == .server) "Server" else "Proxy") else "",
        .scheme = if (challenge.scheme) |s| (if (s == .digest) "digest" else "basic") else "",
        .realm  = challenge.realm orelse "",
    },
    .networkId = &id.toRequestId(transfer.id),
}, .{ .session_id = session_id });

The client must respond with Fetch.continueWithAuth. The handler in src/cdp/domains/fetch.zig (lines 124-169) processes this response by either canceling the challenge or providing credentials:

if (params.authChallengeResponse.response != .ProvideCredentials) {
    transfer.abortAuthChallenge();
    return cmd.sendResult(null, .{});
}
…
transfer.updateCredentials(…);
transfer.reset();
try bc.cdp.browser.http_client.continueTransfer(transfer);

For custom authentication schemes, Lightpanda provides WebBotAuth.signRequest in src/network/WebBotAuth.zig (lines 90-154), which adds Ed25519 signature headers (Signature-Agent, Signature-Input, Signature) to outgoing requests before they reach the network layer.

Practical Implementation Example

The following Zig example demonstrates enabling request interception with authentication handling, modifying requests with custom headers, and responding to authentication challenges:

const std = @import("std");
const cdp = @import("cdp");

// Enable fetch with auth handling
pub fn enableFetch(session: *cdp.Session) !void {
    try session.fetchEnable(true); // true enables auth challenge listening
}

// Handle paused requests
pub fn onRequestPaused(params: cdp.Fetch.RequestPaused) !void {
    // Example: Add WebBotAuth signature headers
    const auth = try cdp.WebBotAuth.fromConfig(std.heap.page_allocator, &myConfig);
    defer auth.deinit(std.heap.page_allocator);
    try auth.signRequest(std.heap.page_allocator, &params.request.headers, params.request.url);
    
    // Continue with modified headers
    try session.continueRequest(.{
        .requestId = params.requestId,
        .headers = params.request.headers,
    });
}

// Handle authentication challenges
pub fn onAuthRequired(params: cdp.Fetch.AuthRequired) !void {
    try session.continueWithAuth(.{
        .requestId = params.requestId,
        .authChallengeResponse = .{
            .response = .ProvideCredentials,
            .username = "user",
            .password = "pass",
        },
    });
}

// Setup registration
pub fn setup(session: *cdp.Session) !void {
    try enableFetch(session);
    try session.notification.register(.http_request_intercept, session, onRequestPaused);
    try session.notification.register(.http_request_auth_required, session, onAuthRequired);
}

This implementation enables full programmatic control over network traffic, from request modification to automated credential provision.

Summary

  • Lightpanda intercepts requests at the HTTP client level by dispatching notification events from src/browser/HttpClient.zig that pause transfers via the wait_for_interception flag.
  • A pub/sub notification system in src/Notification.zig decouples network events from CDP handling, allowing multiple listeners to react to interception and authentication events.
  • The CDP Fetch domain in src/cdp/domains/fetch.zig translates internal events into standard Fetch.requestPaused and Fetch.authRequired protocol events.
  • Clients control paused requests through CDP commands like continueRequest, continueWithAuth, and fulfillRequest, which resume or abort transfers stored in the per-session InterceptState map.
  • Authentication challenges follow the same interception pattern but use distinct notification types and handlers that support both standard HTTP auth and custom WebBotAuth Ed25519 signatures.

Frequently Asked Questions

How does Lightpanda decide whether to pause a request for interception?

Lightpanda checks the wait_for_interception boolean flag after dispatching the .http_request_intercept notification in src/browser/HttpClient.zig (lines 95-110). If a registered CDP listener sets this flag to true, the request enters a pending intercept state and blocks until the client issues a continuation command. If no listener modifies the flag, the request proceeds immediately to the network layer.

What CDP commands does Lightpanda support for controlling intercepted requests?

According to src/cdp/domains/fetch.zig, Lightpanda implements Fetch.continueRequest for modifying request parameters, Fetch.continueWithAuth for providing authentication credentials, Fetch.fulfillRequest for returning mock responses, and Fetch.failRequest for aborting requests. Each command retrieves the stored Transfer from the InterceptState hash map and either resumes or terminates the network operation.

Can Lightpanda handle both Basic and Digest authentication schemes?

Yes. The Fetch.authRequired event in src/cdp/domains/fetch.zig (lines 85-124) includes an authChallenge object that specifies the scheme as either "basic" or "digest", along with the realm and source (server or proxy). The client responds via continueWithAuth with the appropriate credentials, which the handler applies through transfer.updateCredentials() before resetting and continuing the transfer.

Where does custom request signing fit into the interception flow?

The WebBotAuth.signRequest function in src/network/WebBotAuth.zig operates before the standard interception flow, adding Ed25519 signature headers to requests during the modification phase of continueRequest. This allows clients to implement custom authentication schemes alongside or in place of standard HTTP authentication, with signatures applied programmatically before the request resumes through http_client.continueTransfer.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →