From 9579f727b355867e2d0b49ae5cf6c343d5689f45 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 10 Dec 2025 18:21:32 +0100 Subject: [PATCH 1/5] cdp: add mimeType and charset to Network.Response --- src/browser/mime.zig | 12 ++++++++++++ src/cdp/domains/network.zig | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/browser/mime.zig b/src/browser/mime.zig index 812c17d3..793e3cef 100644 --- a/src/browser/mime.zig +++ b/src/browser/mime.zig @@ -53,6 +53,18 @@ pub const Mime = struct { other: struct { type: []const u8, sub_type: []const u8 }, }; + pub fn contentTypeString(mime: *const Mime) [:0]const u8 { + return switch (mime.content_type) { + .text_xml => "text/xml", + .text_html => "text/html", + .text_javascript => "application/javascript", + .text_plain => "text/plain", + .text_css => "text/css", + .application_json => "application/json", + else => "", + }; + } + /// Returns the null-terminated charset value. pub fn charsetString(mime: *const Mime) [:0]const u8 { return @ptrCast(&mime.charset); diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index 0d7014d0..a4f97ff9 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -22,6 +22,7 @@ const Allocator = std.mem.Allocator; const CdpStorage = @import("storage.zig"); const Transfer = @import("../../http/Client.zig").Transfer; const Notification = @import("../../notification.zig").Notification; +const Mime = @import("../../browser/mime.zig").Mime; pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { @@ -392,6 +393,20 @@ const TransferAsResponseWriter = struct { try jws.write(@as(std.http.Status, @enumFromInt(status)).phrase() orelse "Unknown"); } + { + const mime: Mime = blk: { + if (transfer.response_header.?.contentType()) |ct| { + break :blk try Mime.parse(ct); + } + break :blk .unknown; + }; + + try jws.objectField("mimeType"); + try jws.write(mime.contentTypeString()); + try jws.objectField("charset"); + try jws.write(mime.charsetString()[0..]); + } + { // chromedp doesn't like having duplicate header names. It's pretty // common to get these from a server (e.g. for Cache-Control), but From 34518dfa98c03722c1127ab8e088747566650422 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 10 Dec 2025 18:22:44 +0100 Subject: [PATCH 2/5] cdp: add missing fields to Network.requestWillBeSent --- src/cdp/domains/network.zig | 6 +++++- src/http/Client.zig | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index a4f97ff9..9ad62809 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -248,9 +248,12 @@ pub fn httpRequestStart(arena: Allocator, bc: anytype, msg: *const Notification. .requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}), .frameId = target_id, .loaderId = bc.loader_id, - .documentUrl = DocumentUrlWriter.init(&page.url.uri), + .type = msg.transfer.req.resource_type.string(), + .documentURL = DocumentUrlWriter.init(&page.url.uri), .request = TransferAsRequestWriter.init(transfer), .initiator = .{ .type = "other" }, + .redirectHasExtraInfo = false, // TODO change after adding Network.requestWillBeSentExtraInfo + .hasUserGesture = false, }, .{ .session_id = session_id }); } @@ -266,6 +269,7 @@ pub fn httpResponseHeaderDone(arena: Allocator, bc: anytype, msg: *const Notific .loaderId = bc.loader_id, .frameId = target_id, .response = TransferAsResponseWriter.init(arena, msg.transfer), + .hasExtraInfo = false, // TODO change after adding Network.responseReceivedExtraInfo }, .{ .session_id = session_id }); } diff --git a/src/http/Client.zig b/src/http/Client.zig index 588e0c4f..9c564181 100644 --- a/src/http/Client.zig +++ b/src/http/Client.zig @@ -679,6 +679,19 @@ pub const Request = struct { xhr, script, fetch, + + // Allowed Values: Document, Stylesheet, Image, Media, Font, Script, + // TextTrack, XHR, Fetch, Prefetch, EventSource, WebSocket, Manifest, + // SignedExchange, Ping, CSPViolationReport, Preflight, FedCM, Other + // https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-ResourceType + pub fn string(self: ResourceType) []const u8 { + return switch (self) { + .document => "Document", + .xhr => "XHR", + .script => "Script", + .fetch => "Fetch", + }; + } }; }; From e2682ab9fe8d318348c1584359aa34489d0aac6c Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 11 Dec 2025 17:51:17 +0100 Subject: [PATCH 3/5] cdp: dispatch Page.navigate response after navigation --- src/browser/page.zig | 20 ++++++++++++++ src/cdp/cdp.zig | 6 ++-- src/cdp/domains/page.zig | 60 ++++++++++++++++++++++++---------------- src/notification.zig | 13 +++++---- 4 files changed, 66 insertions(+), 33 deletions(-) diff --git a/src/browser/page.zig b/src/browser/page.zig index e33e1c7a..2175f884 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -83,6 +83,7 @@ pub const Page = struct { // indicates intention to navigate to another page on the next loop execution. delayed_navigation: bool = false, + navigated_options: ?NavigatedOpts = null, state_pool: *std.heap.MemoryPool(State), @@ -574,6 +575,11 @@ pub const Page = struct { }); self.session.browser.notification.dispatch(.page_navigated, &.{ + .opts = .{ + .cdp_id = opts.cdp_id, + .reason = opts.reason, + .method = opts.method, + }, .url = request_url, .timestamp = timestamp(), }); @@ -584,6 +590,12 @@ pub const Page = struct { const owned_url = try self.arena.dupeZ(u8, request_url); self.url = try URL.parse(owned_url, null); + self.navigated_options = .{ + .cdp_id = opts.cdp_id, + .reason = opts.reason, + .method = opts.method, + }; + var headers = try self.http_client.newHeaders(); if (opts.header) |hdr| try headers.add(hdr); try self.requestCookie(.{ .is_navigation = true }).headersForRequest(self.arena, owned_url, &headers); @@ -656,7 +668,9 @@ pub const Page = struct { log.err(.browser, "document is complete", .{ .err = err }); }; + std.debug.assert(self.navigated_options != null); self.session.browser.notification.dispatch(.page_navigated, &.{ + .opts = self.navigated_options.?, .url = self.url.raw, .timestamp = timestamp(), }); @@ -1264,6 +1278,12 @@ pub const NavigateOpts = struct { force: bool = false, }; +pub const NavigatedOpts = struct { + cdp_id: ?i64 = null, + reason: NavigateReason = .address_bar, + method: Http.Method = .GET, +}; + const IdleNotification = union(enum) { // hasn't started yet. init, diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index def56773..635f0a3a 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -545,13 +545,13 @@ pub fn BrowserContext(comptime CDP_T: type) type { pub fn onPageNavigate(ctx: *anyopaque, msg: *const Notification.PageNavigate) !void { const self: *Self = @ptrCast(@alignCast(ctx)); - defer self.resetNotificationArena(); - return @import("domains/page.zig").pageNavigate(self.notification_arena, self, msg); + return @import("domains/page.zig").pageNavigate(self, msg); } pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void { const self: *Self = @ptrCast(@alignCast(ctx)); - return @import("domains/page.zig").pageNavigated(self, msg); + defer self.resetNotificationArena(); + return @import("domains/page.zig").pageNavigated(self.notification_arena, self, msg); } pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void { diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 93cf5a26..91e37fb7 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -184,7 +184,7 @@ fn navigate(cmd: anytype) !void { }); } -pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.PageNavigate) !void { +pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void { // detachTarget could be called, in which case, we still have a page doing // things, but no session. const session_id = bc.session_id orelse return; @@ -234,6 +234,30 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa try cdp.sendEvent("Page.frameStartedLoading", .{ .frameId = target_id, }, .{ .session_id = session_id }); +} + +pub fn pageRemove(bc: anytype) !void { + // The main page is going to be removed, we need to remove contexts from other worlds first. + for (bc.isolated_worlds.items) |*isolated_world| { + try isolated_world.removeContext(); + } +} + +pub fn pageCreated(bc: anytype, page: *Page) !void { + for (bc.isolated_worlds.items) |*isolated_world| { + try isolated_world.createContextAndLoadPolyfills(bc.arena, page); + } +} + +pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.PageNavigated) !void { + // detachTarget could be called, in which case, we still have a page doing + // things, but no session. + const session_id = bc.session_id orelse return; + const loader_id = bc.loader_id; + const target_id = bc.target_id orelse unreachable; + const timestamp = event.timestamp; + + var cdp = bc.cdp; // Drivers are sensitive to the order of events. Some more than others. // The result for the Page.navigate seems like it _must_ come after @@ -260,6 +284,17 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa }, .{ .session_id = session_id }); } + const reason_: ?[]const u8 = switch (event.opts.reason) { + .anchor => "anchorClick", + .script, .history, .navigation => "scriptInitiated", + .form => switch (event.opts.method) { + .GET => "formSubmissionGet", + .POST => "formSubmissionPost", + else => unreachable, + }, + .address_bar => null, + }; + if (reason_ != null) { try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{ .frameId = target_id, @@ -293,30 +328,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa false, ); } -} -pub fn pageRemove(bc: anytype) !void { - // The main page is going to be removed, we need to remove contexts from other worlds first. - for (bc.isolated_worlds.items) |*isolated_world| { - try isolated_world.removeContext(); - } -} - -pub fn pageCreated(bc: anytype, page: *Page) !void { - for (bc.isolated_worlds.items) |*isolated_world| { - try isolated_world.createContextAndLoadPolyfills(bc.arena, page); - } -} - -pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !void { - // detachTarget could be called, in which case, we still have a page doing - // things, but no session. - const session_id = bc.session_id orelse return; - const loader_id = bc.loader_id; - const target_id = bc.target_id orelse unreachable; - const timestamp = event.timestamp; - - var cdp = bc.cdp; // frameNavigated event try cdp.sendEvent("Page.frameNavigated", .{ .type = "Navigation", diff --git a/src/notification.zig b/src/notification.zig index 05024166..70d48bbe 100644 --- a/src/notification.zig +++ b/src/notification.zig @@ -98,6 +98,7 @@ pub const Notification = struct { pub const PageNavigated = struct { timestamp: u32, url: []const u8, + opts: page.NavigatedOpts, }; pub const PageNetworkIdle = struct { @@ -326,7 +327,7 @@ test "Notification" { .url = undefined, .opts = .{}, }); - notifier.dispatch(.page_navigated, &.{ .timestamp = 6, .url = undefined }); + notifier.dispatch(.page_navigated, &.{ .timestamp = 6, .url = undefined, .opts = .{} }); try testing.expectEqual(14, tc.page_navigate); try testing.expectEqual(6, tc.page_navigated); @@ -336,7 +337,7 @@ test "Notification" { .url = undefined, .opts = .{}, }); - notifier.dispatch(.page_navigated, &.{ .timestamp = 100, .url = undefined }); + notifier.dispatch(.page_navigated, &.{ .timestamp = 100, .url = undefined, .opts = .{} }); try testing.expectEqual(14, tc.page_navigate); try testing.expectEqual(6, tc.page_navigated); @@ -345,26 +346,26 @@ test "Notification" { try notifier.register(.page_navigate, &tc, TestClient.pageNavigate); try notifier.register(.page_navigated, &tc, TestClient.pageNavigated); notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} }); - notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined }); + notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined, .opts = .{} }); try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(1006, tc.page_navigated); notifier.unregister(.page_navigate, &tc); notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} }); - notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined }); + notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined, .opts = .{} }); try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(2006, tc.page_navigated); notifier.unregister(.page_navigated, &tc); notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} }); - notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined }); + notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined, .opts = .{} }); try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(2006, tc.page_navigated); // already unregistered, try anyways notifier.unregister(.page_navigated, &tc); notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} }); - notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined }); + notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined, .opts = .{} }); try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(2006, tc.page_navigated); } From 26827efe34e726f16362c3b79ef3803086e7113c Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 12 Dec 2025 17:04:18 +0100 Subject: [PATCH 4/5] cdp: use same value for requestId and loaderId For all events regarding an HTTP request, the values of requestId and loaderId must be the same. --- src/browser/page.zig | 13 +++++++++++++ src/cdp/cdp.zig | 3 ++- src/cdp/domains/network.zig | 12 ++++++++---- src/cdp/domains/page.zig | 10 ++++------ src/http/Client.zig | 13 +++++++++++-- src/notification.zig | 27 +++++++++++++++++---------- 6 files changed, 55 insertions(+), 23 deletions(-) diff --git a/src/browser/page.zig b/src/browser/page.zig index 2175f884..05bd528f 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -83,6 +83,7 @@ pub const Page = struct { // indicates intention to navigate to another page on the next loop execution. delayed_navigation: bool = false, + req_id: ?usize = null, navigated_options: ?NavigatedOpts = null, state_pool: *std.heap.MemoryPool(State), @@ -547,11 +548,14 @@ pub const Page = struct { try self.reset(); } + const req_id = self.http_client.nextReqId(); + log.info(.http, "navigate", .{ .url = request_url, .method = opts.method, .reason = opts.reason, .body = opts.body != null, + .req_id = req_id, }); // if the url is about:blank, we load an empty HTML document in the @@ -569,12 +573,14 @@ pub const Page = struct { self.documentIsComplete(); self.session.browser.notification.dispatch(.page_navigate, &.{ + .req_id = req_id, .opts = opts, .url = request_url, .timestamp = timestamp(), }); self.session.browser.notification.dispatch(.page_navigated, &.{ + .req_id = req_id, .opts = .{ .cdp_id = opts.cdp_id, .reason = opts.reason, @@ -584,12 +590,16 @@ pub const Page = struct { .timestamp = timestamp(), }); + // force next request id manually b/c we won't create a real req. + _ = self.http_client.incrReqId(); + return; } const owned_url = try self.arena.dupeZ(u8, request_url); self.url = try URL.parse(owned_url, null); + self.req_id = req_id; self.navigated_options = .{ .cdp_id = opts.cdp_id, .reason = opts.reason, @@ -603,6 +613,7 @@ pub const Page = struct { // We dispatch page_navigate event before sending the request. // It ensures the event page_navigated is not dispatched before this one. self.session.browser.notification.dispatch(.page_navigate, &.{ + .req_id = req_id, .opts = opts, .url = owned_url, .timestamp = timestamp(), @@ -668,8 +679,10 @@ pub const Page = struct { log.err(.browser, "document is complete", .{ .err = err }); }; + std.debug.assert(self.req_id != null); std.debug.assert(self.navigated_options != null); self.session.browser.notification.dispatch(.page_navigated, &.{ + .req_id = self.req_id.?, .opts = self.navigated_options.?, .url = self.url.raw, .timestamp = timestamp(), diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 635f0a3a..a7f46fcb 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -545,7 +545,8 @@ pub fn BrowserContext(comptime CDP_T: type) type { pub fn onPageNavigate(ctx: *anyopaque, msg: *const Notification.PageNavigate) !void { const self: *Self = @ptrCast(@alignCast(ctx)); - return @import("domains/page.zig").pageNavigate(self, msg); + defer self.resetNotificationArena(); + return @import("domains/page.zig").pageNavigate(self.notification_arena, self, msg); } pub fn onPageNavigated(ctx: *anyopaque, msg: *const Notification.PageNavigated) !void { diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index 9ad62809..3d32f191 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -243,11 +243,12 @@ pub fn httpRequestStart(arena: Allocator, bc: anytype, msg: *const Notification. } const transfer = msg.transfer; + const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}); // We're missing a bunch of fields, but, for now, this seems like enough try bc.cdp.sendEvent("Network.requestWillBeSent", .{ - .requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}), + .requestId = loader_id, .frameId = target_id, - .loaderId = bc.loader_id, + .loaderId = loader_id, .type = msg.transfer.req.resource_type.string(), .documentURL = DocumentUrlWriter.init(&page.url.uri), .request = TransferAsRequestWriter.init(transfer), @@ -263,11 +264,14 @@ pub fn httpResponseHeaderDone(arena: Allocator, bc: anytype, msg: *const Notific const session_id = bc.session_id orelse return; const target_id = bc.target_id orelse unreachable; + const transfer = msg.transfer; + const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}); + // We're missing a bunch of fields, but, for now, this seems like enough try bc.cdp.sendEvent("Network.responseReceived", .{ - .requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{msg.transfer.id}), - .loaderId = bc.loader_id, + .requestId = loader_id, .frameId = target_id, + .loaderId = loader_id, .response = TransferAsResponseWriter.init(arena, msg.transfer), .hasExtraInfo = false, // TODO change after adding Network.responseReceivedExtraInfo }, .{ .session_id = session_id }); diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 91e37fb7..8a1e9702 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -176,7 +176,6 @@ fn navigate(cmd: anytype) !void { } var page = bc.session.currentPage() orelse return error.PageNotLoaded; - bc.loader_id = bc.cdp.loader_id_gen.next(); try page.navigate(params.url, .{ .reason = .address_bar, @@ -184,13 +183,12 @@ fn navigate(cmd: anytype) !void { }); } -pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void { +pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.PageNavigate) !void { // detachTarget could be called, in which case, we still have a page doing // things, but no session. const session_id = bc.session_id orelse return; - bc.loader_id = bc.cdp.loader_id_gen.next(); - const loader_id = bc.loader_id; + const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{event.req_id}); const target_id = bc.target_id orelse unreachable; bc.reset(); @@ -253,7 +251,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P // detachTarget could be called, in which case, we still have a page doing // things, but no session. const session_id = bc.session_id orelse return; - const loader_id = bc.loader_id; + const loader_id = try std.fmt.allocPrint(arena, "REQ-{d}", .{event.req_id}); const target_id = bc.target_id orelse unreachable; const timestamp = event.timestamp; @@ -335,7 +333,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P .frame = Frame{ .id = target_id, .url = event.url, - .loaderId = bc.loader_id, + .loaderId = loader_id, .securityOrigin = bc.security_origin, .secureContextType = bc.secure_context_type, }, diff --git a/src/http/Client.zig b/src/http/Client.zig index 9c564181..953df53b 100644 --- a/src/http/Client.zig +++ b/src/http/Client.zig @@ -261,6 +261,16 @@ pub fn fulfillTransfer(self: *Client, transfer: *Transfer, status: u16, headers: return transfer.fulfill(status, headers, body); } +pub fn nextReqId(self: *Client) usize { + return self.next_request_id + 1; +} + +pub fn incrReqId(self: *Client) usize { + const id = self.next_request_id + 1; + self.next_request_id = id; + return id; +} + fn makeTransfer(self: *Client, req: Request) !*Transfer { errdefer req.headers.deinit(); @@ -273,8 +283,7 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer { const transfer = try self.transfer_pool.create(); errdefer self.transfer_pool.destroy(transfer); - const id = self.next_request_id + 1; - self.next_request_id = id; + const id = self.incrReqId(); transfer.* = .{ .arena = ArenaAllocator.init(self.allocator), .id = id, diff --git a/src/notification.zig b/src/notification.zig index 70d48bbe..5da980cf 100644 --- a/src/notification.zig +++ b/src/notification.zig @@ -90,12 +90,14 @@ pub const Notification = struct { pub const PageRemove = struct {}; pub const PageNavigate = struct { + req_id: usize, timestamp: u32, url: []const u8, opts: page.NavigateOpts, }; pub const PageNavigated = struct { + req_id: usize, timestamp: u32, url: []const u8, opts: page.NavigatedOpts, @@ -297,6 +299,7 @@ test "Notification" { // noop notifier.dispatch(.page_navigate, &.{ + .req_id = 1, .timestamp = 4, .url = undefined, .opts = .{}, @@ -306,6 +309,7 @@ test "Notification" { try notifier.register(.page_navigate, &tc, TestClient.pageNavigate); notifier.dispatch(.page_navigate, &.{ + .req_id = 1, .timestamp = 4, .url = undefined, .opts = .{}, @@ -314,6 +318,7 @@ test "Notification" { notifier.unregisterAll(&tc); notifier.dispatch(.page_navigate, &.{ + .req_id = 1, .timestamp = 10, .url = undefined, .opts = .{}, @@ -323,21 +328,23 @@ test "Notification" { try notifier.register(.page_navigate, &tc, TestClient.pageNavigate); try notifier.register(.page_navigated, &tc, TestClient.pageNavigated); notifier.dispatch(.page_navigate, &.{ + .req_id = 1, .timestamp = 10, .url = undefined, .opts = .{}, }); - notifier.dispatch(.page_navigated, &.{ .timestamp = 6, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 6, .url = undefined, .opts = .{} }); try testing.expectEqual(14, tc.page_navigate); try testing.expectEqual(6, tc.page_navigated); notifier.unregisterAll(&tc); notifier.dispatch(.page_navigate, &.{ + .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{}, }); - notifier.dispatch(.page_navigated, &.{ .timestamp = 100, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); try testing.expectEqual(14, tc.page_navigate); try testing.expectEqual(6, tc.page_navigated); @@ -345,27 +352,27 @@ test "Notification" { // unregister try notifier.register(.page_navigate, &tc, TestClient.pageNavigate); try notifier.register(.page_navigated, &tc, TestClient.pageNavigated); - notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} }); - notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} }); try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(1006, tc.page_navigated); notifier.unregister(.page_navigate, &tc); - notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} }); - notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} }); try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(2006, tc.page_navigated); notifier.unregister(.page_navigated, &tc); - notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} }); - notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} }); try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(2006, tc.page_navigated); // already unregistered, try anyways notifier.unregister(.page_navigated, &tc); - notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} }); - notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigate, &.{ .req_id = 1, .timestamp = 100, .url = undefined, .opts = .{} }); + notifier.dispatch(.page_navigated, &.{ .req_id = 1, .timestamp = 1000, .url = undefined, .opts = .{} }); try testing.expectEqual(114, tc.page_navigate); try testing.expectEqual(2006, tc.page_navigated); } From 42440f1503d54add536e51e675a449ed371b8293 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 12 Dec 2025 18:00:20 +0100 Subject: [PATCH 5/5] fix mime.charsetString() --- src/browser/mime.zig | 18 +++++++++++++----- src/browser/page.zig | 4 ++-- src/browser/xhr/xhr.zig | 2 +- src/cdp/domains/network.zig | 2 +- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/browser/mime.zig b/src/browser/mime.zig index 793e3cef..97ac88f6 100644 --- a/src/browser/mime.zig +++ b/src/browser/mime.zig @@ -24,6 +24,7 @@ pub const Mime = struct { // IANA defines max. charset value length as 40. // We keep 41 for null-termination since HTML parser expects in this format. charset: [41]u8 = default_charset, + charset_len: usize = 5, /// String "UTF-8" continued by null characters. pub const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36; @@ -53,7 +54,7 @@ pub const Mime = struct { other: struct { type: []const u8, sub_type: []const u8 }, }; - pub fn contentTypeString(mime: *const Mime) [:0]const u8 { + pub fn contentTypeString(mime: *const Mime) []const u8 { return switch (mime.content_type) { .text_xml => "text/xml", .text_html => "text/html", @@ -66,8 +67,12 @@ pub const Mime = struct { } /// Returns the null-terminated charset value. - pub fn charsetString(mime: *const Mime) [:0]const u8 { - return @ptrCast(&mime.charset); + pub fn charsetStringZ(mime: *const Mime) [:0]const u8 { + return mime.charset[0..mime.charset_len :0]; + } + + pub fn charsetString(mime: *const Mime) []const u8 { + return mime.charset[0..mime.charset_len]; } /// Removes quotes of value if quotes are given. @@ -111,6 +116,7 @@ pub const Mime = struct { const params = trimLeft(normalized[type_len..]); var charset: [41]u8 = undefined; + var charset_len: usize = undefined; var it = std.mem.splitScalar(u8, params, ';'); while (it.next()) |attr| { @@ -136,6 +142,7 @@ pub const Mime = struct { @memcpy(charset[0..attribute_value.len], attribute_value); // Null-terminate right after attribute value. charset[attribute_value.len] = 0; + charset_len = attribute_value.len; }, } } @@ -143,6 +150,7 @@ pub const Mime = struct { return .{ .params = params, .charset = charset, + .charset_len = charset_len, .content_type = content_type, }; } @@ -523,9 +531,9 @@ fn expect(expected: Expectation, input: []const u8) !void { if (expected.charset) |ec| { // We remove the null characters for testing purposes here. - try testing.expectEqual(ec, actual.charsetString()[0..ec.len]); + try testing.expectEqual(ec, actual.charsetString()); } else { const m: Mime = .unknown; - try testing.expectEqual(m.charsetString(), actual.charsetString()); + try testing.expectEqual(m.charsetStringZ(), actual.charsetStringZ()); } } diff --git a/src/browser/page.zig b/src/browser/page.zig index 05bd528f..7281a9c6 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -740,14 +740,14 @@ pub const Page = struct { log.debug(.http, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len }); self.mode = switch (mime.content_type) { - .text_html => .{ .html = try parser.Parser.init(mime.charsetString()) }, + .text_html => .{ .html = try parser.Parser.init(mime.charsetStringZ()) }, .application_json, .text_javascript, .text_css, .text_plain, => blk: { - var p = try parser.Parser.init(mime.charsetString()); + var p = try parser.Parser.init(mime.charsetStringZ()); try p.process("
");
                     break :blk .{ .text = p };
                 },
diff --git a/src/browser/xhr/xhr.zig b/src/browser/xhr/xhr.zig
index ee6a6c04..dfc9ca99 100644
--- a/src/browser/xhr/xhr.zig
+++ b/src/browser/xhr/xhr.zig
@@ -678,7 +678,7 @@ pub const XMLHttpRequest = struct {
         }
 
         var fbs = std.io.fixedBufferStream(self.response_bytes.items);
-        const doc = parser.documentHTMLParse(fbs.reader(), mime.charsetString()) catch {
+        const doc = parser.documentHTMLParse(fbs.reader(), mime.charsetStringZ()) catch {
             self.response_obj = .{ .Failure = {} };
             return;
         };
diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig
index 3d32f191..0b435e7d 100644
--- a/src/cdp/domains/network.zig
+++ b/src/cdp/domains/network.zig
@@ -412,7 +412,7 @@ const TransferAsResponseWriter = struct {
             try jws.objectField("mimeType");
             try jws.write(mime.contentTypeString());
             try jws.objectField("charset");
-            try jws.write(mime.charsetString()[0..]);
+            try jws.write(mime.charsetString());
         }
 
         {