Implement Network.getResponseBody

Add response_data event, CDP now captures the full body so that it can respond
to the Network.getResponseBody. This isn't memory efficient, but I don't see
another way to do it. At least this way, it's only capturing/storing every
response body when (a) CDP is used and (b) Network.enabled is called. That is,
as opposed to baking this into Http/Client.zig, which would force the memory
consumption for all use-cases.

There's arguably some optimizations we could make for XHR requests, which also
dupe/own the response. As of now, the response is dupe'd separately for CDP
and XHR.
This commit is contained in:
Karl Seguin
2025-08-21 10:32:03 +08:00
parent 557f8444b2
commit cd33e9ad0e
4 changed files with 84 additions and 45 deletions

View File

@@ -344,6 +344,15 @@ pub fn BrowserContext(comptime CDP_T: type) type {
intercept_state: InterceptState,
// When network is enabled, we'll capture the transfer.id -> body
// This is awfully memory intensive, but our underlying http client and
// its users (script manager and page) correctly do not hold the body
// memory longer than they have to. In fact, the main request is only
// ever streamed. So if CDP is the only thing that needs bodies in
// memory for an arbitrary amount of time, then that's where we're going
// to store the,
captured_responses: std.AutoHashMapUnmanaged(usize, std.ArrayListUnmanaged(u8)),
const Self = @This();
fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void {
@@ -374,6 +383,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
.inspector = inspector,
.notification_arena = cdp.notification_arena.allocator(),
.intercept_state = try InterceptState.init(allocator),
.captured_responses = .empty,
};
self.node_search_list = Node.Search.List.init(allocator, &self.node_registry);
errdefer self.deinit();
@@ -454,15 +464,17 @@ pub fn BrowserContext(comptime CDP_T: type) type {
pub fn networkEnable(self: *Self) !void {
try self.cdp.browser.notification.register(.http_request_fail, self, onHttpRequestFail);
try self.cdp.browser.notification.register(.http_request_start, self, onHttpRequestStart);
try self.cdp.browser.notification.register(.http_headers_done, self, onHttpHeadersDone);
try self.cdp.browser.notification.register(.http_request_done, self, onHttpRequestDone);
try self.cdp.browser.notification.register(.http_response_data, self, onHttpResponseData);
try self.cdp.browser.notification.register(.http_response_header_done, self, onHttpResponseHeadersDone);
}
pub fn networkDisable(self: *Self) void {
self.cdp.browser.notification.unregister(.http_request_fail, self);
self.cdp.browser.notification.unregister(.http_request_start, self);
self.cdp.browser.notification.unregister(.http_headers_done, self);
self.cdp.browser.notification.unregister(.http_request_done, self);
self.cdp.browser.notification.unregister(.http_response_data, self);
self.cdp.browser.notification.unregister(.http_response_header_done, self);
}
pub fn fetchEnable(self: *Self) !void {
@@ -483,45 +495,57 @@ pub fn BrowserContext(comptime CDP_T: type) type {
return @import("domains/page.zig").pageCreated(self, page);
}
pub fn onPageNavigate(ctx: *anyopaque, data: *const Notification.PageNavigate) !void {
pub fn onPageNavigate(ctx: *anyopaque, msg: *const Notification.PageNavigate) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
defer self.resetNotificationArena();
return @import("domains/page.zig").pageNavigate(self.notification_arena, self, data);
return @import("domains/page.zig").pageNavigate(self.notification_arena, self, msg);
}
pub fn onPageNavigated(ctx: *anyopaque, data: *const Notification.PageNavigated) !void {
pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
return @import("domains/page.zig").pageNavigated(self, data);
return @import("domains/page.zig").pageNavigated(self, msg);
}
pub fn onHttpRequestStart(ctx: *anyopaque, data: *const Notification.RequestStart) !void {
pub fn onHttpRequestStart(ctx: *anyopaque, msg: *const Notification.RequestStart) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
defer self.resetNotificationArena();
try @import("domains/network.zig").httpRequestStart(self.notification_arena, self, data);
try @import("domains/network.zig").httpRequestStart(self.notification_arena, self, msg);
}
pub fn onHttpRequestIntercept(ctx: *anyopaque, data: *const Notification.RequestIntercept) !void {
pub fn onHttpRequestIntercept(ctx: *anyopaque, msg: *const Notification.RequestIntercept) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
defer self.resetNotificationArena();
try @import("domains/fetch.zig").requestIntercept(self.notification_arena, self, data);
try @import("domains/fetch.zig").requestIntercept(self.notification_arena, self, msg);
}
pub fn onHttpRequestFail(ctx: *anyopaque, data: *const Notification.RequestFail) !void {
pub fn onHttpRequestFail(ctx: *anyopaque, msg: *const Notification.RequestFail) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
defer self.resetNotificationArena();
return @import("domains/network.zig").httpRequestFail(self.notification_arena, self, data);
return @import("domains/network.zig").httpRequestFail(self.notification_arena, self, msg);
}
pub fn onHttpHeadersDone(ctx: *anyopaque, data: *const Notification.ResponseHeadersDone) !void {
pub fn onHttpResponseHeadersDone(ctx: *anyopaque, msg: *const Notification.ResponseHeaderDone) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
defer self.resetNotificationArena();
return @import("domains/network.zig").httpHeadersDone(self.notification_arena, self, data);
return @import("domains/network.zig").httpResponseHeaderDone(self.notification_arena, self, msg);
}
pub fn onHttpRequestDone(ctx: *anyopaque, data: *const Notification.RequestDone) !void {
pub fn onHttpRequestDone(ctx: *anyopaque, msg: *const Notification.RequestDone) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
defer self.resetNotificationArena();
return @import("domains/network.zig").httpRequestDone(self.notification_arena, self, data);
return @import("domains/network.zig").httpRequestDone(self.notification_arena, self, msg);
}
pub fn onHttpResponseData(ctx: *anyopaque, msg: *const Notification.ResponseData) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
const arena = self.arena;
const id = msg.transfer.id;
const gop = try self.captured_responses.getOrPut(arena, id);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
try gop.value_ptr.appendSlice(arena, try arena.dupe(u8, msg.data));
}
fn resetNotificationArena(self: *Self) void {