Skip to content

Commit cd33e9a

Browse files
committed
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.
1 parent 557f844 commit cd33e9a

File tree

4 files changed

+84
-45
lines changed

4 files changed

+84
-45
lines changed

src/cdp/cdp.zig

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,15 @@ pub fn BrowserContext(comptime CDP_T: type) type {
344344

345345
intercept_state: InterceptState,
346346

347+
// When network is enabled, we'll capture the transfer.id -> body
348+
// This is awfully memory intensive, but our underlying http client and
349+
// its users (script manager and page) correctly do not hold the body
350+
// memory longer than they have to. In fact, the main request is only
351+
// ever streamed. So if CDP is the only thing that needs bodies in
352+
// memory for an arbitrary amount of time, then that's where we're going
353+
// to store the,
354+
captured_responses: std.AutoHashMapUnmanaged(usize, std.ArrayListUnmanaged(u8)),
355+
347356
const Self = @This();
348357

349358
fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void {
@@ -374,6 +383,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
374383
.inspector = inspector,
375384
.notification_arena = cdp.notification_arena.allocator(),
376385
.intercept_state = try InterceptState.init(allocator),
386+
.captured_responses = .empty,
377387
};
378388
self.node_search_list = Node.Search.List.init(allocator, &self.node_registry);
379389
errdefer self.deinit();
@@ -454,15 +464,17 @@ pub fn BrowserContext(comptime CDP_T: type) type {
454464
pub fn networkEnable(self: *Self) !void {
455465
try self.cdp.browser.notification.register(.http_request_fail, self, onHttpRequestFail);
456466
try self.cdp.browser.notification.register(.http_request_start, self, onHttpRequestStart);
457-
try self.cdp.browser.notification.register(.http_headers_done, self, onHttpHeadersDone);
458467
try self.cdp.browser.notification.register(.http_request_done, self, onHttpRequestDone);
468+
try self.cdp.browser.notification.register(.http_response_data, self, onHttpResponseData);
469+
try self.cdp.browser.notification.register(.http_response_header_done, self, onHttpResponseHeadersDone);
459470
}
460471

461472
pub fn networkDisable(self: *Self) void {
462473
self.cdp.browser.notification.unregister(.http_request_fail, self);
463474
self.cdp.browser.notification.unregister(.http_request_start, self);
464-
self.cdp.browser.notification.unregister(.http_headers_done, self);
465475
self.cdp.browser.notification.unregister(.http_request_done, self);
476+
self.cdp.browser.notification.unregister(.http_response_data, self);
477+
self.cdp.browser.notification.unregister(.http_response_header_done, self);
466478
}
467479

468480
pub fn fetchEnable(self: *Self) !void {
@@ -483,45 +495,57 @@ pub fn BrowserContext(comptime CDP_T: type) type {
483495
return @import("domains/page.zig").pageCreated(self, page);
484496
}
485497

486-
pub fn onPageNavigate(ctx: *anyopaque, data: *const Notification.PageNavigate) !void {
498+
pub fn onPageNavigate(ctx: *anyopaque, msg: *const Notification.PageNavigate) !void {
487499
const self: *Self = @alignCast(@ptrCast(ctx));
488500
defer self.resetNotificationArena();
489-
return @import("domains/page.zig").pageNavigate(self.notification_arena, self, data);
501+
return @import("domains/page.zig").pageNavigate(self.notification_arena, self, msg);
490502
}
491503

492-
pub fn onPageNavigated(ctx: *anyopaque, data: *const Notification.PageNavigated) !void {
504+
pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void {
493505
const self: *Self = @alignCast(@ptrCast(ctx));
494-
return @import("domains/page.zig").pageNavigated(self, data);
506+
return @import("domains/page.zig").pageNavigated(self, msg);
495507
}
496508

497-
pub fn onHttpRequestStart(ctx: *anyopaque, data: *const Notification.RequestStart) !void {
509+
pub fn onHttpRequestStart(ctx: *anyopaque, msg: *const Notification.RequestStart) !void {
498510
const self: *Self = @alignCast(@ptrCast(ctx));
499511
defer self.resetNotificationArena();
500-
try @import("domains/network.zig").httpRequestStart(self.notification_arena, self, data);
512+
try @import("domains/network.zig").httpRequestStart(self.notification_arena, self, msg);
501513
}
502514

503-
pub fn onHttpRequestIntercept(ctx: *anyopaque, data: *const Notification.RequestIntercept) !void {
515+
pub fn onHttpRequestIntercept(ctx: *anyopaque, msg: *const Notification.RequestIntercept) !void {
504516
const self: *Self = @alignCast(@ptrCast(ctx));
505517
defer self.resetNotificationArena();
506-
try @import("domains/fetch.zig").requestIntercept(self.notification_arena, self, data);
518+
try @import("domains/fetch.zig").requestIntercept(self.notification_arena, self, msg);
507519
}
508520

509-
pub fn onHttpRequestFail(ctx: *anyopaque, data: *const Notification.RequestFail) !void {
521+
pub fn onHttpRequestFail(ctx: *anyopaque, msg: *const Notification.RequestFail) !void {
510522
const self: *Self = @alignCast(@ptrCast(ctx));
511523
defer self.resetNotificationArena();
512-
return @import("domains/network.zig").httpRequestFail(self.notification_arena, self, data);
524+
return @import("domains/network.zig").httpRequestFail(self.notification_arena, self, msg);
513525
}
514526

515-
pub fn onHttpHeadersDone(ctx: *anyopaque, data: *const Notification.ResponseHeadersDone) !void {
527+
pub fn onHttpResponseHeadersDone(ctx: *anyopaque, msg: *const Notification.ResponseHeaderDone) !void {
516528
const self: *Self = @alignCast(@ptrCast(ctx));
517529
defer self.resetNotificationArena();
518-
return @import("domains/network.zig").httpHeadersDone(self.notification_arena, self, data);
530+
return @import("domains/network.zig").httpResponseHeaderDone(self.notification_arena, self, msg);
519531
}
520532

521-
pub fn onHttpRequestDone(ctx: *anyopaque, data: *const Notification.RequestDone) !void {
533+
pub fn onHttpRequestDone(ctx: *anyopaque, msg: *const Notification.RequestDone) !void {
522534
const self: *Self = @alignCast(@ptrCast(ctx));
523535
defer self.resetNotificationArena();
524-
return @import("domains/network.zig").httpRequestDone(self.notification_arena, self, data);
536+
return @import("domains/network.zig").httpRequestDone(self.notification_arena, self, msg);
537+
}
538+
539+
pub fn onHttpResponseData(ctx: *anyopaque, msg: *const Notification.ResponseData) !void {
540+
const self: *Self = @alignCast(@ptrCast(ctx));
541+
const arena = self.arena;
542+
543+
const id = msg.transfer.id;
544+
const gop = try self.captured_responses.getOrPut(arena, id);
545+
if (!gop.found_existing) {
546+
gop.value_ptr.* = .{};
547+
}
548+
try gop.value_ptr.appendSlice(arena, try arena.dupe(u8, msg.data));
525549
}
526550

527551
fn resetNotificationArena(self: *Self) void {

src/cdp/domains/network.zig

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,6 @@ pub fn processMessage(cmd: anytype) !void {
5454
}
5555
}
5656

57-
const Response = struct {
58-
status: u16,
59-
headers: std.StringArrayHashMapUnmanaged([]const u8) = .empty,
60-
// These may not be complete yet, but we only tell the client
61-
// Network.responseReceived when all the headers are in.
62-
// Later should store body as well to support getResponseBody which should
63-
// only work once Network.loadingFinished is sent but the body itself would
64-
// be loaded with each chunks as Network.dataReceiveds are coming in.
65-
};
66-
6757
fn enable(cmd: anytype) !void {
6858
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
6959
try bc.networkEnable();
@@ -209,15 +199,17 @@ fn getResponseBody(cmd: anytype) !void {
209199
requestId: []const u8, // "REQ-{d}"
210200
})) orelse return error.InvalidParams;
211201

212-
_ = params;
202+
const request_id = try idFromRequestId(params.requestId);
203+
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
204+
const buf = bc.captured_responses.getPtr(request_id) orelse return error.RequestNotFound;
213205

214206
try cmd.sendResult(.{
215-
.body = "TODO",
207+
.body = buf.items,
216208
.base64Encoded = false,
217209
}, .{});
218210
}
219211

220-
pub fn httpRequestFail(arena: Allocator, bc: anytype, data: *const Notification.RequestFail) !void {
212+
pub fn httpRequestFail(arena: Allocator, bc: anytype, msg: *const Notification.RequestFail) !void {
221213
// It's possible that the request failed because we aborted when the client
222214
// sent Target.closeTarget. In that case, bc.session_id will be cleared
223215
// already, and we can skip sending these messages to the client.
@@ -229,15 +221,15 @@ pub fn httpRequestFail(arena: Allocator, bc: anytype, data: *const Notification.
229221

230222
// We're missing a bunch of fields, but, for now, this seems like enough
231223
try bc.cdp.sendEvent("Network.loadingFailed", .{
232-
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{data.transfer.id}),
224+
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{msg.transfer.id}),
233225
// Seems to be what chrome answers with. I assume it depends on the type of error?
234226
.type = "Ping",
235-
.errorText = data.err,
227+
.errorText = msg.err,
236228
.canceled = false,
237229
}, .{ .session_id = session_id });
238230
}
239231

240-
pub fn httpRequestStart(arena: Allocator, bc: anytype, data: *const Notification.RequestStart) !void {
232+
pub fn httpRequestStart(arena: Allocator, bc: anytype, msg: *const Notification.RequestStart) !void {
241233
// Isn't possible to do a network request within a Browser (which our
242234
// notification is tied to), without a page.
243235
std.debug.assert(bc.session.page != null);
@@ -251,15 +243,15 @@ pub fn httpRequestStart(arena: Allocator, bc: anytype, data: *const Notification
251243

252244
// Modify request with extra CDP headers
253245
for (bc.extra_headers.items) |extra| {
254-
try data.transfer.req.headers.add(extra);
246+
try msg.transfer.req.headers.add(extra);
255247
}
256248

257-
const transfer = data.transfer;
249+
const transfer = msg.transfer;
258250
// We're missing a bunch of fields, but, for now, this seems like enough
259251
try cdp.sendEvent("Network.requestWillBeSent", .{ .requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}), .frameId = target_id, .loaderId = bc.loader_id, .documentUrl = DocumentUrlWriter.init(&page.url.uri), .request = TransferAsRequestWriter.init(transfer) }, .{ .session_id = session_id });
260252
}
261253

262-
pub fn httpHeadersDone(arena: Allocator, bc: anytype, data: *const Notification.ResponseHeadersDone) !void {
254+
pub fn httpResponseHeaderDone(arena: Allocator, bc: anytype, msg: *const Notification.ResponseHeaderDone) !void {
263255
// Isn't possible to do a network request within a Browser (which our
264256
// notification is tied to), without a page.
265257
std.debug.assert(bc.session.page != null);
@@ -272,14 +264,14 @@ pub fn httpHeadersDone(arena: Allocator, bc: anytype, data: *const Notification.
272264

273265
// We're missing a bunch of fields, but, for now, this seems like enough
274266
try cdp.sendEvent("Network.responseReceived", .{
275-
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{data.transfer.id}),
267+
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{msg.transfer.id}),
276268
.loaderId = bc.loader_id,
277269
.frameId = target_id,
278-
.response = TransferAsResponseWriter.init(data.transfer),
270+
.response = TransferAsResponseWriter.init(msg.transfer),
279271
}, .{ .session_id = session_id });
280272
}
281273

282-
pub fn httpRequestDone(arena: Allocator, bc: anytype, data: *const Notification.RequestDone) !void {
274+
pub fn httpRequestDone(arena: Allocator, bc: anytype, msg: *const Notification.RequestDone) !void {
283275
// Isn't possible to do a network request within a Browser (which our
284276
// notification is tied to), without a page.
285277
std.debug.assert(bc.session.page != null);
@@ -290,8 +282,8 @@ pub fn httpRequestDone(arena: Allocator, bc: anytype, data: *const Notification.
290282
const session_id = bc.session_id orelse unreachable;
291283

292284
try cdp.sendEvent("Network.loadingFinished", .{
293-
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{data.transfer.id}),
294-
.encodedDataLength = data.transfer.bytes_received,
285+
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{msg.transfer.id}),
286+
.encodedDataLength = msg.transfer.bytes_received,
295287
}, .{ .session_id = session_id });
296288
}
297289

@@ -439,6 +431,13 @@ const DocumentUrlWriter = struct {
439431
}
440432
};
441433

434+
fn idFromRequestId(request_id: []const u8) !u64 {
435+
if (!std.mem.startsWith(u8, request_id, "REQ-")) {
436+
return error.InvalidParams;
437+
}
438+
return std.fmt.parseInt(u64, request_id[4..], 10) catch return error.InvalidParams;
439+
}
440+
442441
const testing = @import("../testing.zig");
443442
test "cdp.network setExtraHTTPHeaders" {
444443
var ctx = testing.context();

src/http/Client.zig

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,7 @@ pub const Transfer = struct {
757757
};
758758

759759
if (transfer.client.notification) |notification| {
760-
notification.dispatch(.http_headers_done, &.{
760+
notification.dispatch(.http_response_header_done, &.{
761761
.transfer = transfer,
762762
});
763763
}
@@ -780,10 +780,19 @@ pub const Transfer = struct {
780780
}
781781

782782
transfer.bytes_received += chunk_len;
783-
transfer.req.data_callback(transfer, buffer[0..chunk_len]) catch |err| {
783+
const chunk = buffer[0..chunk_len];
784+
transfer.req.data_callback(transfer, chunk) catch |err| {
784785
log.err(.http, "data_callback", .{ .err = err, .req = transfer });
785786
return c.CURL_WRITEFUNC_ERROR;
786787
};
788+
789+
if (transfer.client.notification) |notification| {
790+
notification.dispatch(.http_response_data, &.{
791+
.data = chunk,
792+
.transfer = transfer,
793+
});
794+
}
795+
787796
return chunk_len;
788797
}
789798

src/notification.zig

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ pub const Notification = struct {
6363
http_request_fail: List = .{},
6464
http_request_start: List = .{},
6565
http_request_intercept: List = .{},
66-
http_headers_done: List = .{},
6766
http_request_done: List = .{},
67+
http_response_data: List = .{},
68+
http_response_header_done: List = .{},
6869
notification_created: List = .{},
6970
};
7071

@@ -76,8 +77,9 @@ pub const Notification = struct {
7677
http_request_fail: *const RequestFail,
7778
http_request_start: *const RequestStart,
7879
http_request_intercept: *const RequestIntercept,
79-
http_headers_done: *const ResponseHeadersDone,
8080
http_request_done: *const RequestDone,
81+
http_response_data: *const ResponseData,
82+
http_response_header_done: *const ResponseHeaderDone,
8183
notification_created: *Notification,
8284
};
8385
const EventType = std.meta.FieldEnum(Events);
@@ -104,7 +106,12 @@ pub const Notification = struct {
104106
wait_for_interception: *bool,
105107
};
106108

107-
pub const ResponseHeadersDone = struct {
109+
pub const ResponseData = struct {
110+
data: []const u8,
111+
transfer: *Transfer,
112+
};
113+
114+
pub const ResponseHeaderDone = struct {
108115
transfer: *Transfer,
109116
};
110117

0 commit comments

Comments
 (0)