From b8f1622b52db133f6e9e046388bd818e964e5d9a Mon Sep 17 00:00:00 2001 From: gilangjavier Date: Thu, 19 Mar 2026 13:34:44 +0700 Subject: [PATCH 1/6] fix(cdp): base64-encode binary Network.getResponseBody payloads --- src/cdp/domains/network.zig | 56 +++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index 5b9a49df..392f1c7a 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -210,9 +210,21 @@ fn getResponseBody(cmd: anytype) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const buf = bc.captured_responses.getPtr(request_id) orelse return error.RequestNotFound; + if (std.unicode.utf8ValidateSlice(buf.items)) { + try cmd.sendResult(.{ + .body = buf.items, + .base64Encoded = false, + }, .{}); + return; + } + + const encoded_len = std.base64.standard.Encoder.calcSize(buf.items.len); + const encoded = try cmd.arena.alloc(u8, encoded_len); + _ = std.base64.standard.Encoder.encode(encoded, buf.items); + try cmd.sendResult(.{ - .body = buf.items, - .base64Encoded = false, + .body = encoded, + .base64Encoded = true, }, .{}); } @@ -523,3 +535,43 @@ test "cdp.Network: cookies" { }); try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 10 }); } + +test "cdp.Network.getResponseBody returns plain text for UTF-8 responses" { + var ctx = testing.context(); + defer ctx.deinit(); + + const bc = try ctx.loadBrowserContext(.{ .id = "BID-GRB", .session_id = "NESI-GRB" }); + var text_response: std.ArrayList(u8) = .{}; + try text_response.appendSlice(bc.arena, "hello"); + try bc.captured_responses.put(bc.arena, 42, text_response); + + try ctx.processMessage(.{ + .id = 11, + .method = "Network.getResponseBody", + .params = .{ .requestId = "REQ-42" }, + }); + try ctx.expectSentResult(.{ + .body = "hello", + .base64Encoded = false, + }, .{ .id = 11 }); +} + +test "cdp.Network.getResponseBody base64-encodes non-UTF8 responses" { + var ctx = testing.context(); + defer ctx.deinit(); + + const bc = try ctx.loadBrowserContext(.{ .id = "BID-GRB-BIN", .session_id = "NESI-GRB-BIN" }); + var binary_response: std.ArrayList(u8) = .{}; + try binary_response.appendSlice(bc.arena, &[_]u8{ 0xFF, 0x00, 0x41 }); + try bc.captured_responses.put(bc.arena, 43, binary_response); + + try ctx.processMessage(.{ + .id = 12, + .method = "Network.getResponseBody", + .params = .{ .requestId = "REQ-43" }, + }); + try ctx.expectSentResult(.{ + .body = "/wBB", + .base64Encoded = true, + }, .{ .id = 12 }); +} From b5b012bd5d2f530e55fa8d9d3e626bc598df364f Mon Sep 17 00:00:00 2001 From: gilangjavier Date: Sat, 21 Mar 2026 07:06:09 +0700 Subject: [PATCH 2/6] refactor(cdp): always return base64-encoded Network.getResponseBody --- src/cdp/domains/network.zig | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index 392f1c7a..b9875737 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -210,14 +210,6 @@ fn getResponseBody(cmd: anytype) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const buf = bc.captured_responses.getPtr(request_id) orelse return error.RequestNotFound; - if (std.unicode.utf8ValidateSlice(buf.items)) { - try cmd.sendResult(.{ - .body = buf.items, - .base64Encoded = false, - }, .{}); - return; - } - const encoded_len = std.base64.standard.Encoder.calcSize(buf.items.len); const encoded = try cmd.arena.alloc(u8, encoded_len); _ = std.base64.standard.Encoder.encode(encoded, buf.items); @@ -536,7 +528,7 @@ test "cdp.Network: cookies" { try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 10 }); } -test "cdp.Network.getResponseBody returns plain text for UTF-8 responses" { +test "cdp.Network.getResponseBody base64-encodes UTF-8 responses" { var ctx = testing.context(); defer ctx.deinit(); @@ -551,8 +543,8 @@ test "cdp.Network.getResponseBody returns plain text for UTF-8 responses" { .params = .{ .requestId = "REQ-42" }, }); try ctx.expectSentResult(.{ - .body = "hello", - .base64Encoded = false, + .body = "aGVsbG8=", + .base64Encoded = true, }, .{ .id = 11 }); } From 2107ade3a5e15f4db961a6e7fc35f8d275f23b2c Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Sat, 21 Mar 2026 13:11:18 +0100 Subject: [PATCH 3/6] use a CapturedResponse struct for captured responses --- src/cdp/cdp.zig | 14 +++++++++++--- src/cdp/domains/network.zig | 10 +++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 58ed11b9..4cf50876 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -324,6 +324,11 @@ pub fn BrowserContext(comptime CDP_T: type) type { const Node = @import("Node.zig"); const AXNode = @import("AXNode.zig"); + const CapturedResponse = struct { + encode: enum { none, base64 }, + data: std.ArrayList(u8), + }; + return struct { id: []const u8, cdp: *CDP_T, @@ -384,7 +389,7 @@ pub fn BrowserContext(comptime CDP_T: type) type { // 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.ArrayList(u8)), + captured_responses: std.AutoHashMapUnmanaged(usize, CapturedResponse), notification: *Notification, @@ -648,9 +653,12 @@ pub fn BrowserContext(comptime CDP_T: type) type { const id = msg.transfer.id; const gop = try self.captured_responses.getOrPut(arena, id); if (!gop.found_existing) { - gop.value_ptr.* = .{}; + gop.value_ptr.* = .{ + .data = .empty, + .encode = .none, + }; } - try gop.value_ptr.appendSlice(arena, try arena.dupe(u8, msg.data)); + try gop.value_ptr.data.appendSlice(arena, try arena.dupe(u8, msg.data)); } pub fn onHttpRequestAuthRequired(ctx: *anyopaque, data: *const Notification.RequestAuthRequired) !void { diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index b9875737..8b143e32 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -208,15 +208,11 @@ fn getResponseBody(cmd: anytype) !void { const request_id = try idFromRequestId(params.requestId); const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const buf = bc.captured_responses.getPtr(request_id) orelse return error.RequestNotFound; - - const encoded_len = std.base64.standard.Encoder.calcSize(buf.items.len); - const encoded = try cmd.arena.alloc(u8, encoded_len); - _ = std.base64.standard.Encoder.encode(encoded, buf.items); + const resp = bc.captured_responses.getPtr(request_id) orelse return error.RequestNotFound; try cmd.sendResult(.{ - .body = encoded, - .base64Encoded = true, + .body = resp.data.items, + .base64Encoded = resp.encode == .base64, }, .{}); } From 00d06dbe8ce81127a72e68396b6386447ada288f Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Sat, 21 Mar 2026 13:29:58 +0100 Subject: [PATCH 4/6] encode all captured responses body in base64 --- src/cdp/cdp.zig | 12 +++++++++-- src/cdp/domains/network.zig | 40 ------------------------------------- 2 files changed, 10 insertions(+), 42 deletions(-) diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 4cf50876..2dbbc29d 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -655,10 +655,18 @@ pub fn BrowserContext(comptime CDP_T: type) type { if (!gop.found_existing) { gop.value_ptr.* = .{ .data = .empty, - .encode = .none, + .encode = .base64, }; } - try gop.value_ptr.data.appendSlice(arena, try arena.dupe(u8, msg.data)); + + // Always base64 ecncode the catured response body. + // TODO: use the response's content-type to decide to encode or not + // the body. + const resp = gop.value_ptr; + const encoded_len = std.base64.standard.Encoder.calcSize(msg.data.len); + const start = resp.data.items.len; + try resp.data.resize(arena, start + encoded_len); + _ = std.base64.standard.Encoder.encode(resp.data.items[start..], msg.data); } pub fn onHttpRequestAuthRequired(ctx: *anyopaque, data: *const Notification.RequestAuthRequired) !void { diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index 8b143e32..f26e2037 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -523,43 +523,3 @@ test "cdp.Network: cookies" { }); try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 10 }); } - -test "cdp.Network.getResponseBody base64-encodes UTF-8 responses" { - var ctx = testing.context(); - defer ctx.deinit(); - - const bc = try ctx.loadBrowserContext(.{ .id = "BID-GRB", .session_id = "NESI-GRB" }); - var text_response: std.ArrayList(u8) = .{}; - try text_response.appendSlice(bc.arena, "hello"); - try bc.captured_responses.put(bc.arena, 42, text_response); - - try ctx.processMessage(.{ - .id = 11, - .method = "Network.getResponseBody", - .params = .{ .requestId = "REQ-42" }, - }); - try ctx.expectSentResult(.{ - .body = "aGVsbG8=", - .base64Encoded = true, - }, .{ .id = 11 }); -} - -test "cdp.Network.getResponseBody base64-encodes non-UTF8 responses" { - var ctx = testing.context(); - defer ctx.deinit(); - - const bc = try ctx.loadBrowserContext(.{ .id = "BID-GRB-BIN", .session_id = "NESI-GRB-BIN" }); - var binary_response: std.ArrayList(u8) = .{}; - try binary_response.appendSlice(bc.arena, &[_]u8{ 0xFF, 0x00, 0x41 }); - try bc.captured_responses.put(bc.arena, 43, binary_response); - - try ctx.processMessage(.{ - .id = 12, - .method = "Network.getResponseBody", - .params = .{ .requestId = "REQ-43" }, - }); - try ctx.expectSentResult(.{ - .body = "/wBB", - .base64Encoded = true, - }, .{ .id = 12 }); -} From 30f387d36144dca9ae72d00243c8939ef17ad167 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Sat, 21 Mar 2026 14:08:08 +0100 Subject: [PATCH 5/6] encode captured response depending of the content type --- src/browser/Mime.zig | 8 ++++++++ src/cdp/cdp.zig | 44 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/browser/Mime.zig b/src/browser/Mime.zig index e23d48a2..eca97a2d 100644 --- a/src/browser/Mime.zig +++ b/src/browser/Mime.zig @@ -386,6 +386,14 @@ pub fn isHTML(self: *const Mime) bool { return self.content_type == .text_html; } +pub fn isText(mime: *const Mime) bool { + return switch (mime.content_type) { + .text_xml, .text_html, .text_javascript, .text_plain, .text_css => true, + .application_json => true, + else => false, + }; +} + // we expect value to be lowercase fn parseContentType(value: []const u8) !struct { ContentType, usize } { const end = std.mem.indexOfScalarPos(u8, value, 0, ';') orelse value.len; diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 2dbbc29d..e003e935 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -33,6 +33,7 @@ const Page = @import("../browser/Page.zig"); const Incrementing = @import("id.zig").Incrementing; const Notification = @import("../Notification.zig"); const InterceptState = @import("domains/fetch.zig").InterceptState; +const Mime = @import("../browser/Mime.zig"); pub const URL_BASE = "chrome://newtab/"; @@ -638,6 +639,35 @@ pub fn BrowserContext(comptime CDP_T: type) type { pub fn onHttpResponseHeadersDone(ctx: *anyopaque, msg: *const Notification.ResponseHeaderDone) !void { const self: *Self = @ptrCast(@alignCast(ctx)); defer self.resetNotificationArena(); + + const arena = self.page_arena; + + // Prepare the captured response value. + const id = msg.transfer.id; + const gop = try self.captured_responses.getOrPut(arena, id); + if (!gop.found_existing) { + gop.value_ptr.* = .{ + .data = .empty, + // Encode the data in base64 by default, but use none + // encoding for well known content-type. + .encode = blk: { + const transfer = msg.transfer; + if (transfer.response_header.?.contentType()) |ct| { + const mime = try Mime.parse(ct); + + if (!mime.isText()) { + break :blk .base64; + } + + if (std.mem.eql(u8, "UTF-8", mime.charsetString())) { + break :blk .none; + } + } + break :blk .base64; + }, + }; + } + return @import("domains/network.zig").httpResponseHeaderDone(self.notification_arena, self, msg); } @@ -651,18 +681,12 @@ pub fn BrowserContext(comptime CDP_T: type) type { const arena = self.page_arena; const id = msg.transfer.id; - const gop = try self.captured_responses.getOrPut(arena, id); - if (!gop.found_existing) { - gop.value_ptr.* = .{ - .data = .empty, - .encode = .base64, - }; + const resp = self.captured_responses.getPtr(id) orelse lp.assert(false, "onHttpResponseData missinf captured response", .{}); + + if (resp.encode == .none) { + return resp.data.appendSlice(arena, msg.data); } - // Always base64 ecncode the catured response body. - // TODO: use the response's content-type to decide to encode or not - // the body. - const resp = gop.value_ptr; const encoded_len = std.base64.standard.Encoder.calcSize(msg.data.len); const start = resp.data.items.len; try resp.data.resize(arena, start + encoded_len); From 797cae2ef8409200980b94a0f4e636be16e79bf2 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 23 Mar 2026 14:26:27 +0100 Subject: [PATCH 6/6] encode captured response body during CDP call --- src/cdp/cdp.zig | 23 ++++++++--------------- src/cdp/domains/network.zig | 17 ++++++++++++++--- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index e003e935..b55a923d 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -326,7 +326,7 @@ pub fn BrowserContext(comptime CDP_T: type) type { const AXNode = @import("AXNode.zig"); const CapturedResponse = struct { - encode: enum { none, base64 }, + must_encode: bool, data: std.ArrayList(u8), }; @@ -648,22 +648,22 @@ pub fn BrowserContext(comptime CDP_T: type) type { if (!gop.found_existing) { gop.value_ptr.* = .{ .data = .empty, - // Encode the data in base64 by default, but use none - // encoding for well known content-type. - .encode = blk: { + // Encode the data in base64 by default, but don't encode + // for well known content-type. + .must_encode = blk: { const transfer = msg.transfer; if (transfer.response_header.?.contentType()) |ct| { const mime = try Mime.parse(ct); if (!mime.isText()) { - break :blk .base64; + break :blk true; } if (std.mem.eql(u8, "UTF-8", mime.charsetString())) { - break :blk .none; + break :blk false; } } - break :blk .base64; + break :blk true; }, }; } @@ -683,14 +683,7 @@ pub fn BrowserContext(comptime CDP_T: type) type { const id = msg.transfer.id; const resp = self.captured_responses.getPtr(id) orelse lp.assert(false, "onHttpResponseData missinf captured response", .{}); - if (resp.encode == .none) { - return resp.data.appendSlice(arena, msg.data); - } - - const encoded_len = std.base64.standard.Encoder.calcSize(msg.data.len); - const start = resp.data.items.len; - try resp.data.resize(arena, start + encoded_len); - _ = std.base64.standard.Encoder.encode(resp.data.items[start..], msg.data); + return resp.data.appendSlice(arena, msg.data); } pub fn onHttpRequestAuthRequired(ctx: *anyopaque, data: *const Notification.RequestAuthRequired) !void { diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index f26e2037..11e0142c 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -210,9 +210,20 @@ fn getResponseBody(cmd: anytype) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const resp = bc.captured_responses.getPtr(request_id) orelse return error.RequestNotFound; - try cmd.sendResult(.{ - .body = resp.data.items, - .base64Encoded = resp.encode == .base64, + if (!resp.must_encode) { + return cmd.sendResult(.{ + .body = resp.data.items, + .base64Encoded = false, + }, .{}); + } + + const encoded_len = std.base64.standard.Encoder.calcSize(resp.data.items.len); + const encoded = try cmd.arena.alloc(u8, encoded_len); + _ = std.base64.standard.Encoder.encode(encoded, resp.data.items); + + return cmd.sendResult(.{ + .body = encoded, + .base64Encoded = true, }, .{}); }