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: HandlercontinueRequest(lines 226-269) updates URL, method, headers, or body before callinghttp_client.continueTransfer.Fetch.failRequest: Handler callshttp_client.abortTransferto abort the request.Fetch.fulfillRequest: HandlerfulfillRequestsends 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, ¶ms.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.zigthat pause transfers via thewait_for_interceptionflag. - A pub/sub notification system in
src/Notification.zigdecouples network events from CDP handling, allowing multiple listeners to react to interception and authentication events. - The CDP Fetch domain in
src/cdp/domains/fetch.zigtranslates internal events into standardFetch.requestPausedandFetch.authRequiredprotocol events. - Clients control paused requests through CDP commands like
continueRequest,continueWithAuth, andfulfillRequest, 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →