From 8b310ce993974d4aec34947ff4b81fec97269901 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 10 Mar 2026 19:17:51 +0300 Subject: [PATCH 01/24] add failing `body.onload` tests --- src/browser/tests/window/body_onload2.html | 14 +++++------ src/browser/tests/window/body_onload3.html | 28 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 src/browser/tests/window/body_onload3.html diff --git a/src/browser/tests/window/body_onload2.html b/src/browser/tests/window/body_onload2.html index 32327c93..0403b30c 100644 --- a/src/browser/tests/window/body_onload2.html +++ b/src/browser/tests/window/body_onload2.html @@ -1,15 +1,15 @@ - + - diff --git a/src/browser/tests/window/body_onload3.html b/src/browser/tests/window/body_onload3.html new file mode 100644 index 00000000..875be388 --- /dev/null +++ b/src/browser/tests/window/body_onload3.html @@ -0,0 +1,28 @@ + + + + + From 099550dddc3056942b95df6798606d7eeb7757bd Mon Sep 17 00:00:00 2001 From: hobostay Date: Sat, 14 Mar 2026 13:18:42 +0800 Subject: [PATCH 02/24] Ignore partitionKey in cookie operations to support Puppeteer page.setCookie() Puppeteer's page.setCookie() internally calls Network.deleteCookies twice before setting a cookie. The second call includes a partitionKey field for CHIPS (partitioned cookies), which caused Lightpanda to return NotImplemented. Since Lightpanda doesn't support partitioned cookies, we now silently ignore the partitionKey parameter and proceed with the cookie operation based on name/domain/path matching. This change affects: - Network.deleteCookies: no longer rejects requests with partitionKey - Network.setCookie (via setCdpCookie): no longer rejects cookies with partitionKey Fixes #1818 Co-Authored-By: Claude Opus 4.6 --- src/cdp/domains/network.zig | 7 ++++++- src/cdp/domains/storage.zig | 10 +++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index c04ee33b..dab07df8 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -117,7 +117,12 @@ fn deleteCookies(cmd: anytype) !void { path: ?[]const u8 = null, partitionKey: ?CdpStorage.CookiePartitionKey = null, })) orelse return error.InvalidParams; - if (params.partitionKey != null) return error.NotImplemented; + // Silently ignore partitionKey since we don't support partitioned cookies (CHIPS). + // This allows Puppeteer's page.setCookie() to work, which sends deleteCookies + // with partitionKey as part of its cookie-setting workflow. + if (params.partitionKey != null) { + log.debug(.network, "partitionKey ignored in deleteCookies", .{}); + } const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const cookies = &bc.session.cookie_jar.cookies; diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig index c7546ad6..d772f736 100644 --- a/src/cdp/domains/storage.zig +++ b/src/cdp/domains/storage.zig @@ -128,7 +128,15 @@ pub const CdpCookie = struct { }; pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void { - if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null or param.partitionKey != null) { + // Silently ignore partitionKey since we don't support partitioned cookies (CHIPS). + // This allows Puppeteer's page.setCookie() to work, which may send cookies with + // partitionKey as part of its cookie-setting workflow. + if (param.partitionKey != null) { + const log = @import("../../log.zig"); + log.debug(.storage, "partitionKey ignored in setCdpCookie", .{}); + } + // Still reject unsupported features + if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null) { return error.NotImplemented; } From 1352839472493b55dd7447d407f4dfef7341336e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 14 Mar 2026 14:02:58 +0800 Subject: [PATCH 03/24] Remove frame double-free on navigate error The explicit deinit isn't needed as here's already an errdefer in play. --- src/browser/Page.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 0599bab5..cb62cb31 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1091,7 +1091,6 @@ pub fn iframeAddedCallback(self: *Page, iframe: *IFrame) !void { log.warn(.page, "iframe navigate failure", .{ .url = url, .err = err }); self._pending_loads -= 1; iframe._window = null; - page_frame.deinit(true); return error.IFrameLoadError; }; From a204f4096803dc7bd60fd1ed542f3eb8828bc93b Mon Sep 17 00:00:00 2001 From: sjhddh Date: Sat, 14 Mar 2026 08:36:06 +0000 Subject: [PATCH 04/24] fix(dom): return parsererror document on XML parse failure --- src/browser/webapi/DOMParser.zig | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/browser/webapi/DOMParser.zig b/src/browser/webapi/DOMParser.zig index 9e27e9c7..10a94bca 100644 --- a/src/browser/webapi/DOMParser.zig +++ b/src/browser/webapi/DOMParser.zig @@ -86,15 +86,15 @@ pub fn parseFromString( var parser = Parser.init(arena, doc_node, page); parser.parseXML(html); - if (parser.err) |pe| { - return pe.err; + if (parser.err != null or doc_node.firstChild() == null) { + // Return a document with a element per spec. + const err_doc = try page._factory.document(XMLDocument{ ._proto = undefined }); + var err_parser = Parser.init(arena, err_doc.asNode(), page); + err_parser.parseXML("error"); + return err_doc.asDocument(); } - const first_child = doc_node.firstChild() orelse { - // Empty XML or no root element - this is a parse error. - // TODO: Return a document with a element per spec. - return error.JsException; - }; + const first_child = doc_node.firstChild().?; // If first node is a `ProcessingInstruction`, skip it. if (first_child.getNodeType() == 7) { From 68337a6989d027fd51cc5e89d87d62c7661e6e91 Mon Sep 17 00:00:00 2001 From: hobostay Date: Sat, 14 Mar 2026 17:32:48 +0800 Subject: [PATCH 05/24] Fix compilation errors: add missing log import and remove duplicate - Add missing `const log = @import("../../log.zig");` in network.zig - Remove duplicate `log` declaration inside setCdpCookie in storage.zig (already declared at file scope) Fixes compilation errors: - src/cdp/domains/network.zig:124:9: error: use of undeclared identifier 'log' - src/cdp/domains/storage.zig:135:15: error: local constant shadows declaration of 'log' Co-Authored-By: Claude Opus 4.6 --- src/cdp/domains/network.zig | 1 + src/cdp/domains/storage.zig | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index dab07df8..5c422a8d 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -19,6 +19,7 @@ const std = @import("std"); const lp = @import("lightpanda"); const Allocator = std.mem.Allocator; +const log = @import("../../log.zig"); const CdpStorage = @import("storage.zig"); diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig index d772f736..497f7efa 100644 --- a/src/cdp/domains/storage.zig +++ b/src/cdp/domains/storage.zig @@ -132,7 +132,6 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void { // This allows Puppeteer's page.setCookie() to work, which may send cookies with // partitionKey as part of its cookie-setting workflow. if (param.partitionKey != null) { - const log = @import("../../log.zig"); log.debug(.storage, "partitionKey ignored in setCdpCookie", .{}); } // Still reject unsupported features From 93ea95af2475c12a6c4c3b44de1938108af95b8d Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:48:29 +0000 Subject: [PATCH 06/24] feat(mcp): add ping request handling --- src/mcp/Server.zig | 21 +++++++++++++++++++++ src/mcp/protocol.zig | 21 +++++++++++++++++++++ src/mcp/router.zig | 29 ++++++++++++++++++++++------- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/mcp/Server.zig b/src/mcp/Server.zig index 6f8b1f21..e3ef8097 100644 --- a/src/mcp/Server.zig +++ b/src/mcp/Server.zig @@ -116,3 +116,24 @@ test "MCP.Server - Integration: synchronous smoke test" { try testing.expectJson(.{ .id = 1 }, out_alloc.writer.buffered()); } + +test "MCP.Server - Integration: ping request returns an empty result" { + defer testing.reset(); + const allocator = testing.allocator; + const app = testing.test_app; + + const input = + \\{"jsonrpc":"2.0","id":"ping-1","method":"ping"} + ; + + var in_reader: std.io.Reader = .fixed(input); + var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator); + defer out_alloc.deinit(); + + var server = try Self.init(allocator, app, &out_alloc.writer); + defer server.deinit(); + + try router.processRequests(server, &in_reader); + + try testing.expectJson(.{ .id = "ping-1", .result = .{} }, out_alloc.writer.buffered()); +} diff --git a/src/mcp/protocol.zig b/src/mcp/protocol.zig index 1c488535..2bc6f9e8 100644 --- a/src/mcp/protocol.zig +++ b/src/mcp/protocol.zig @@ -238,6 +238,27 @@ test "MCP.protocol - request parsing" { try testing.expectString("1.0.0", init_params.value.clientInfo.version); } +test "MCP.protocol - ping request parsing" { + defer testing.reset(); + const raw_json = + \\{ + \\ "jsonrpc": "2.0", + \\ "id": "123", + \\ "method": "ping" + \\} + ; + + const parsed = try std.json.parseFromSlice(Request, testing.arena_allocator, raw_json, .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + + const req = parsed.value; + try testing.expectString("2.0", req.jsonrpc); + try testing.expectString("ping", req.method); + try testing.expect(req.id.? == .string); + try testing.expectString("123", req.id.?.string); + try testing.expectEqual(null, req.params); +} + test "MCP.protocol - response formatting" { defer testing.reset(); const response = Response{ diff --git a/src/mcp/router.zig b/src/mcp/router.zig index 3f4d32ee..70411814 100644 --- a/src/mcp/router.zig +++ b/src/mcp/router.zig @@ -34,6 +34,7 @@ const log = @import("../log.zig"); const Method = enum { initialize, + ping, @"notifications/initialized", @"tools/list", @"tools/call", @@ -43,6 +44,7 @@ const Method = enum { const method_map = std.StaticStringMap(Method).initComptime(.{ .{ "initialize", .initialize }, + .{ "ping", .ping }, .{ "notifications/initialized", .@"notifications/initialized" }, .{ "tools/list", .@"tools/list" }, .{ "tools/call", .@"tools/call" }, @@ -68,6 +70,7 @@ pub fn handleMessage(server: *Server, arena: std.mem.Allocator, msg: []const u8) switch (method) { .initialize => try handleInitialize(server, req), + .ping => try handlePing(server, req), .@"notifications/initialized" => {}, .@"tools/list" => try tools.handleList(server, arena, req), .@"tools/call" => try tools.handleCall(server, arena, req), @@ -92,6 +95,11 @@ fn handleInitialize(server: *Server, req: protocol.Request) !void { try server.sendResult(req.id.?, result); } +fn handlePing(server: *Server, req: protocol.Request) !void { + const id = req.id orelse return; + try server.sendResult(id, .{}); +} + const testing = @import("../testing.zig"); test "MCP.router - handleMessage - synchronous unit tests" { @@ -116,22 +124,29 @@ test "MCP.router - handleMessage - synchronous unit tests" { , out_alloc.writer.buffered()); out_alloc.writer.end = 0; - // 2. Tools list + // 2. Ping try handleMessage(server, aa, - \\{"jsonrpc":"2.0","id":2,"method":"tools/list"} + \\{"jsonrpc":"2.0","id":2,"method":"ping"} ); - try testing.expectJson(.{ .id = 2 }, out_alloc.writer.buffered()); + try testing.expectJson(.{ .id = 2, .result = .{} }, out_alloc.writer.buffered()); + out_alloc.writer.end = 0; + + // 3. Tools list + try handleMessage(server, aa, + \\{"jsonrpc":"2.0","id":3,"method":"tools/list"} + ); + try testing.expectJson(.{ .id = 3 }, out_alloc.writer.buffered()); try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "\"name\":\"goto\"") != null); out_alloc.writer.end = 0; - // 3. Method not found + // 4. Method not found try handleMessage(server, aa, - \\{"jsonrpc":"2.0","id":3,"method":"unknown_method"} + \\{"jsonrpc":"2.0","id":4,"method":"unknown_method"} ); - try testing.expectJson(.{ .id = 3, .@"error" = .{ .code = -32601 } }, out_alloc.writer.buffered()); + try testing.expectJson(.{ .id = 4, .@"error" = .{ .code = -32601 } }, out_alloc.writer.buffered()); out_alloc.writer.end = 0; - // 4. Parse error + // 5. Parse error { const filter: testing.LogFilter = .init(.mcp); defer filter.deinit(); From 5bc00045c7de53dd43568eb2569549f53dbe8d01 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 14 Mar 2026 14:09:49 -0700 Subject: [PATCH 07/24] fix: serialize AXValue integer as string per CDP spec The CDP Accessibility spec defines AXValue.value as always being a string, but integer values were serialized as JSON numbers. This breaks CDP clients with strict deserialization (e.g., Rust serde). Fixes #1822 --- src/cdp/AXNode.zig | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/cdp/AXNode.zig b/src/cdp/AXNode.zig index 718d0bab..ca1c1b98 100644 --- a/src/cdp/AXNode.zig +++ b/src/cdp/AXNode.zig @@ -228,6 +228,12 @@ pub const Writer = struct { try w.objectField("value"); switch (value) { + .integer => |v| { + // CDP spec requires integer values to be serialized as strings. + var buf: [20]u8 = undefined; + const s = try std.fmt.bufPrint(&buf, "{d}", .{v}); + try w.write(s); + }, inline else => |v| try w.write(v), } @@ -1212,4 +1218,25 @@ test "AXNode: writer" { // Check childIds array exists const child_ids = doc_node.get("childIds").?.array.items; try testing.expect(child_ids.len > 0); + + // Find the h1 node and verify its level property is serialized as a string + for (nodes) |node_val| { + const obj = node_val.object; + const role_obj = obj.get("role") orelse continue; + const role_val = role_obj.object.get("value") orelse continue; + if (!std.mem.eql(u8, role_val.string, "heading")) continue; + + const props = obj.get("properties").?.array.items; + for (props) |prop| { + const prop_obj = prop.object; + const name_str = prop_obj.get("name").?.string; + if (!std.mem.eql(u8, name_str, "level")) continue; + const level_value = prop_obj.get("value").?.object; + try testing.expectEqual("integer", level_value.get("type").?.string); + // CDP spec: integer values must be serialized as strings + try testing.expectEqual("1", level_value.get("value").?.string); + return; + } + } + return error.HeadingNodeNotFound; } From 4b6bf29b83d5c191bc14c9ea2e9ab9a546eb7e02 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sun, 15 Mar 2026 09:55:13 +0800 Subject: [PATCH 08/24] Improve ergonomics around rejecting a promise with a proper JS error --- src/browser/js/PromiseResolver.zig | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/browser/js/PromiseResolver.zig b/src/browser/js/PromiseResolver.zig index f2aac0e0..d02740ac 100644 --- a/src/browser/js/PromiseResolver.zig +++ b/src/browser/js/PromiseResolver.zig @@ -63,6 +63,20 @@ pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype }; } +const RejectError = union(enum) { + generic: []const u8, + type_error: []const u8, +}; +pub fn rejectError(self: PromiseResolver, comptime source: []const u8, err: RejectError) void { + const handle = switch (err) { + .type_error => |str| self.local.isolate.createTypeError(str), + .generic => |str| self.local.isolate.createError(str) + }; + self._reject(js.Value{.handle = handle, .local = self.local}) catch |reject_err| { + log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false }); + }; +} + fn _reject(self: PromiseResolver, value: anytype) !void { const local = self.local; const js_val = try local.zigValueToJs(value, .{}); From d4b941cf306279be7a0486f3647b08b4d221bfc3 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sun, 15 Mar 2026 10:06:20 +0800 Subject: [PATCH 09/24] zig fmt --- src/browser/js/PromiseResolver.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/js/PromiseResolver.zig b/src/browser/js/PromiseResolver.zig index d02740ac..67f04311 100644 --- a/src/browser/js/PromiseResolver.zig +++ b/src/browser/js/PromiseResolver.zig @@ -70,9 +70,9 @@ const RejectError = union(enum) { pub fn rejectError(self: PromiseResolver, comptime source: []const u8, err: RejectError) void { const handle = switch (err) { .type_error => |str| self.local.isolate.createTypeError(str), - .generic => |str| self.local.isolate.createError(str) + .generic => |str| self.local.isolate.createError(str), }; - self._reject(js.Value{.handle = handle, .local = self.local}) catch |reject_err| { + self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| { log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false }); }; } From a2e66f85a1e7bad26658752be99eaa5e462d5d6d Mon Sep 17 00:00:00 2001 From: Navid EMAD Date: Sun, 15 Mar 2026 04:36:47 +0100 Subject: [PATCH 10/24] fix(cdp): don't kill WebSocket on unknown domain/method errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a CDP command with an unrecognized domain (e.g. `NonExistent.method`) was sent, the error response was correctly returned but the connection died immediately after. This happened because dispatch() re-returned the error after sending the error response, which propagated up through processMessage() → handleMessage() where `catch return false` closed the WebSocket connection. Now the error is only propagated if sendError itself fails (e.g. broken pipe). Otherwise dispatch() returns normally and the read loop continues. Fixes #1843 Co-Authored-By: Claude Opus 4.6 --- src/cdp/cdp.zig | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 08055d86..58ed11b9 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -168,13 +168,11 @@ pub fn CDPT(comptime TypeProvider: type) type { if (is_startup) { dispatchStartupCommand(&command, input.method) catch |err| { - command.sendError(-31999, @errorName(err), .{}) catch {}; - return err; + command.sendError(-31999, @errorName(err), .{}) catch return err; }; } else { dispatchCommand(&command, input.method) catch |err| { - command.sendError(-31998, @errorName(err), .{}) catch {}; - return err; + command.sendError(-31998, @errorName(err), .{}) catch return err; }; } } @@ -924,18 +922,20 @@ test "cdp: invalid json" { // method is required try testing.expectError(error.InvalidJSON, ctx.processMessage(.{})); - try testing.expectError(error.InvalidMethod, ctx.processMessage(.{ + try ctx.processMessage(.{ .method = "Target", - })); + }); try ctx.expectSentError(-31998, "InvalidMethod", .{}); - try testing.expectError(error.UnknownDomain, ctx.processMessage(.{ + try ctx.processMessage(.{ .method = "Unknown.domain", - })); + }); + try ctx.expectSentError(-31998, "UnknownDomain", .{}); - try testing.expectError(error.UnknownMethod, ctx.processMessage(.{ + try ctx.processMessage(.{ .method = "Target.over9000", - })); + }); + try ctx.expectSentError(-31998, "UnknownMethod", .{}); } test "cdp: invalid sessionId" { From fe9b2e672bce8d27457518cfd550c9fb9bbbc75b Mon Sep 17 00:00:00 2001 From: Navid EMAD Date: Sun, 15 Mar 2026 05:52:20 +0100 Subject: [PATCH 11/24] fix(test): update tests to match new CDP error handling behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit processMessage no longer returns Zig errors when dispatchCommand fails — it sends a CDP error response and continues. Update all expectError calls to use processMessage + expectSentError instead. Co-Authored-By: Claude Opus 4.6 --- src/cdp/domains/dom.zig | 15 +++++++++------ src/cdp/domains/page.zig | 2 +- src/cdp/domains/target.zig | 22 +++++++++++----------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 5150a8e5..386577db 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -550,11 +550,12 @@ test "cdp.dom: getSearchResults unknown search id" { var ctx = testing.context(); defer ctx.deinit(); - try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ + try ctx.processMessage(.{ .id = 8, .method = "DOM.getSearchResults", .params = .{ .searchId = "Nope", .fromIndex = 0, .toIndex = 10 }, - })); + }); + try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 8 }); } test "cdp.dom: search flow" { @@ -604,11 +605,12 @@ test "cdp.dom: search flow" { try ctx.expectSentResult(null, .{ .id = 16 }); // make sure the delete actually did something - try testing.expectError(error.SearchResultNotFound, ctx.processMessage(.{ + try ctx.processMessage(.{ .id = 17, .method = "DOM.getSearchResults", .params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 }, - })); + }); + try ctx.expectSentError(-31998, "SearchResultNotFound", .{ .id = 17 }); } test "cdp.dom: querySelector unknown search id" { @@ -645,11 +647,12 @@ test "cdp.dom: querySelector Node not found" { }); try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 2 }, .{ .id = 3 }); - try testing.expectError(error.NodeNotFoundForGivenId, ctx.processMessage(.{ + try ctx.processMessage(.{ .id = 4, .method = "DOM.querySelector", .params = .{ .nodeId = 1, .selector = "a" }, - })); + }); + try ctx.expectSentError(-31998, "NodeNotFoundForGivenId", .{ .id = 4 }); try ctx.processMessage(.{ .id = 5, diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index fabf37e5..fe21fcca 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -633,7 +633,7 @@ test "cdp.page: getFrameTree" { defer ctx.deinit(); { - try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Page.getFrameTree", .params = .{ .targetId = "X" } })); + try ctx.processMessage(.{ .id = 10, .method = "Page.getFrameTree", .params = .{ .targetId = "X" } }); try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 }); } diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig index 80dc1512..1ff23849 100644 --- a/src/cdp/domains/target.zig +++ b/src/cdp/domains/target.zig @@ -556,7 +556,7 @@ test "cdp.target: disposeBrowserContext" { defer ctx.deinit(); { - try testing.expectError(error.InvalidParams, ctx.processMessage(.{ .id = 7, .method = "Target.disposeBrowserContext" })); + try ctx.processMessage(.{ .id = 7, .method = "Target.disposeBrowserContext" }); try ctx.expectSentError(-31998, "InvalidParams", .{ .id = 7 }); } @@ -609,7 +609,7 @@ test "cdp.target: createTarget" { defer ctx.deinit(); const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" }); { - try testing.expectError(error.UnknownBrowserContextId, ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-8" } })); + try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-8" } }); try ctx.expectSentError(-31998, "UnknownBrowserContextId", .{ .id = 10 }); } @@ -626,13 +626,13 @@ test "cdp.target: closeTarget" { defer ctx.deinit(); { - try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "X" } })); + try ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "X" } }); try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 }); } const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" }); { - try testing.expectError(error.TargetNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } })); + try ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }); try ctx.expectSentError(-31998, "TargetNotLoaded", .{ .id = 10 }); } @@ -640,7 +640,7 @@ test "cdp.target: closeTarget" { _ = try bc.session.createPage(); bc.target_id = "TID-000000000A".*; { - try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } })); + try ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }); try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 }); } @@ -657,13 +657,13 @@ test "cdp.target: attachToTarget" { defer ctx.deinit(); { - try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "X" } })); + try ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "X" } }); try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 }); } const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" }); { - try testing.expectError(error.TargetNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } })); + try ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }); try ctx.expectSentError(-31998, "TargetNotLoaded", .{ .id = 10 }); } @@ -671,7 +671,7 @@ test "cdp.target: attachToTarget" { _ = try bc.session.createPage(); bc.target_id = "TID-000000000B".*; { - try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } })); + try ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }); try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 }); } @@ -701,13 +701,13 @@ test "cdp.target: getTargetInfo" { } { - try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "X" } })); + try ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "X" } }); try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 }); } const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" }); { - try testing.expectError(error.TargetNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } })); + try ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } }); try ctx.expectSentError(-31998, "TargetNotLoaded", .{ .id = 10 }); } @@ -715,7 +715,7 @@ test "cdp.target: getTargetInfo" { _ = try bc.session.createPage(); bc.target_id = "TID-000000000C".*; { - try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } })); + try ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } }); try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 }); } From 3013e3a9e6beaaf67784a550f34d32208f4424ff Mon Sep 17 00:00:00 2001 From: sjhddh Date: Sat, 14 Mar 2026 08:42:02 +0000 Subject: [PATCH 12/24] fix(net): fetch() should reject with a TypeError on network errors --- src/browser/webapi/net/Fetch.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index 9b0f2f98..71b01c56 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -192,7 +192,10 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { self._page.js.localScope(&ls); defer ls.deinit(); - ls.toLocal(self._resolver).reject("fetch error", @errorName(err)); + // fetch() must reject with a TypeError on network errors per spec + const err_handle = ls.local.isolate.createTypeError("Failed to fetch"); + const err_val = js.Value{ .local = ls.local, .handle = err_handle }; + ls.toLocal(self._resolver).reject("fetch error", err_val); } fn httpShutdownCallback(ctx: *anyopaque) void { From 56d3cf51e853a550ad1022a37289a0d61fa4e217 Mon Sep 17 00:00:00 2001 From: sjhddh Date: Sun, 15 Mar 2026 07:12:10 +0000 Subject: [PATCH 13/24] test: update empty xml parse error case in domparser.html --- src/browser/tests/domparser.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/tests/domparser.html b/src/browser/tests/domparser.html index 7930ec87..d8399055 100644 --- a/src/browser/tests/domparser.html +++ b/src/browser/tests/domparser.html @@ -11,9 +11,9 @@ } { - // Empty XML is a parse error (no root element) const parser = new DOMParser(); - testing.expectError('Error', () => parser.parseFromString('', 'text/xml')); + let d = parser.parseFromString('', 'text/xml'); + testing.expectEqual('error', new XMLSerializer().serializeToString(d)); } } From 4d60f56e66b0ded3e1c18266a70aa604bbfd5014 Mon Sep 17 00:00:00 2001 From: sjhddh Date: Sun, 15 Mar 2026 07:26:18 +0000 Subject: [PATCH 14/24] test: add test case for fetch throwing TypeError on network errors --- src/browser/tests/net/fetch.html | 12 ++++++++++++ src/browser/webapi/net/Fetch.zig | 4 +--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/browser/tests/net/fetch.html b/src/browser/tests/net/fetch.html index a545a452..c284e6a8 100644 --- a/src/browser/tests/net/fetch.html +++ b/src/browser/tests/net/fetch.html @@ -203,3 +203,15 @@ testing.expectEqual(true, response.body !== null); }); + + diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index 71b01c56..ab98a8e5 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -193,9 +193,7 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { defer ls.deinit(); // fetch() must reject with a TypeError on network errors per spec - const err_handle = ls.local.isolate.createTypeError("Failed to fetch"); - const err_val = js.Value{ .local = ls.local, .handle = err_handle }; - ls.toLocal(self._resolver).reject("fetch error", err_val); + ls.toLocal(self._resolver).rejectError("fetch error", .{ .type_error = @errorName(err) }); } fn httpShutdownCallback(ctx: *anyopaque) void { From 0243c6b450f0e251f8dc69ecb13f61ca86ec2352 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sun, 15 Mar 2026 21:03:55 +0800 Subject: [PATCH 15/24] Fix issues with blobs https://github.com/lightpanda-io/browser/pull/1775 made blobs finalizable and https://github.com/lightpanda-io/browser/pull/1795 made it possible to navigate from blobs (important for WPT tests). This fixes a number of issues related to both. First, weak/strong ref'ing a value now uses the resolved value. When registering a finalizer, we use the resolved value (the most specific type in the prototype chain). For this reason, when toggling a weak/strong ref, we have to use the same resolved value. This solves a segfault where a File is created, but extended as a Blob (e.g. in createObjectURL). Next, two issues were fixed when navigating to an invalid blob. First, the frame is properly removed from the parent list on frame navigation error. Second, on frame navigation error, we don't stop _all_ other navigations, we just log the error and move on to the next frame. --- src/browser/Session.zig | 21 ++++++++++++++++++--- src/browser/js/Context.zig | 13 ++++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/browser/Session.zig b/src/browser/Session.zig index fea56a87..404a8bc4 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -548,7 +548,9 @@ fn processQueuedNavigation(self: *Session) !void { continue; } - try self.processFrameNavigation(page, qn); + self.processFrameNavigation(page, qn) catch |err| { + log.warn(.page, "frame navigation", .{ .url = qn.url, .err = err }); + }; } // Clear the queue after first pass @@ -588,7 +590,8 @@ fn processFrameNavigation(self: *Session, page: *Page, qn: *QueuedNavigation) !v errdefer iframe._window = null; - if (page._parent_notified) { + const parent_notified = page._parent_notified; + if (parent_notified) { // we already notified the parent that we had loaded parent._pending_loads += 1; } @@ -598,7 +601,19 @@ fn processFrameNavigation(self: *Session, page: *Page, qn: *QueuedNavigation) !v page.* = undefined; try Page.init(page, frame_id, self, parent); - errdefer page.deinit(true); + errdefer { + for (parent.frames.items, 0..) |frame, i| { + if (frame == page) { + parent.frames_sorted = false; + _ = parent.frames.swapRemove(i); + break; + } + } + if (parent_notified) { + parent._pending_loads -= 1; + } + page.deinit(true); + } page.iframe = iframe; iframe._window = page.window; diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 5c58c5cb..70af9d24 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -197,18 +197,20 @@ pub fn trackTemp(self: *Context, global: v8.Global) !void { } pub fn weakRef(self: *Context, obj: anytype) void { - const fc = self.origin.finalizer_callbacks.get(@intFromPtr(obj)) orelse { + const resolved = js.Local.resolveValue(obj); + const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { if (comptime IS_DEBUG) { // should not be possible std.debug.assert(false); } return; }; - v8.v8__Global__SetWeakFinalizer(&fc.global, fc, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter); + v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter); } pub fn safeWeakRef(self: *Context, obj: anytype) void { - const fc = self.origin.finalizer_callbacks.get(@intFromPtr(obj)) orelse { + const resolved = js.Local.resolveValue(obj); + const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { if (comptime IS_DEBUG) { // should not be possible std.debug.assert(false); @@ -216,11 +218,12 @@ pub fn safeWeakRef(self: *Context, obj: anytype) void { return; }; v8.v8__Global__ClearWeak(&fc.global); - v8.v8__Global__SetWeakFinalizer(&fc.global, fc, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter); + v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter); } pub fn strongRef(self: *Context, obj: anytype) void { - const fc = self.origin.finalizer_callbacks.get(@intFromPtr(obj)) orelse { + const resolved = js.Local.resolveValue(obj); + const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { if (comptime IS_DEBUG) { // should not be possible std.debug.assert(false); From 8c52b8357c47d5ae493b3ddcec52555411040416 Mon Sep 17 00:00:00 2001 From: katie-lpd Date: Sun, 15 Mar 2026 16:33:53 +0100 Subject: [PATCH 16/24] Update README.md --- README.md | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 1a860fc7..521c82c0 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@

Logo

-

Lightpanda Browser

+

+The headless browser built from scratch for AI agents and automation.
+Not a Chromium fork. Not a WebKit patch. A new browser, written in Zig. +

-

lightpanda.io

- +
[![License](https://img.shields.io/github/license/lightpanda-io/browser)](https://github.com/lightpanda-io/browser/blob/main/LICENSE) @@ -13,6 +15,17 @@ [![GitHub stars](https://img.shields.io/github/stars/lightpanda-io/browser)](https://github.com/lightpanda-io/browser)
+
+ +[ +](https://github.com/lightpanda-io/demo) +  +[ +](https://github.com/lightpanda-io/demo) +
+ +_Puppeteer requesting 100 pages from a local website on a AWS EC2 m5.large instance. +See [benchmark details](https://github.com/lightpanda-io/demo)._ Lightpanda is the open-source browser made for headless usage: @@ -26,16 +39,6 @@ Fast web automation for AI agents, LLM training, scraping and testing: - Exceptionally fast execution (11x faster than Chrome) - Instant startup -[ -](https://github.com/lightpanda-io/demo) -  -[ -](https://github.com/lightpanda-io/demo) - - -_Puppeteer requesting 100 pages from a local website on a AWS EC2 m5.large instance. -See [benchmark details](https://github.com/lightpanda-io/demo)._ - [^1]: **Playwright support disclaimer:** Due to the nature of Playwright, a script that works with the current version of the browser may not function correctly with a future version. Playwright uses an intermediate JavaScript layer that selects an execution strategy based on the browser's available features. If Lightpanda adds a new [Web API](https://developer.mozilla.org/en-US/docs/Web/API), Playwright may choose to execute different code for the same script. This new code path could attempt to use features that are not yet implemented. Lightpanda makes an effort to add compatibility tests, but we can't cover all scenarios. If you encounter an issue, please create a [GitHub issue](https://github.com/lightpanda-io/browser/issues) and include the last known working version of the script. From a70da0d176fe4a52756a0e651c61c26cc85f0839 Mon Sep 17 00:00:00 2001 From: katie-lpd Date: Sun, 15 Mar 2026 16:57:09 +0100 Subject: [PATCH 17/24] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 521c82c0..456499ce 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Not a Chromium fork. Not a WebKit patch. A new browser, written in Zig. [![License](https://img.shields.io/github/license/lightpanda-io/browser)](https://github.com/lightpanda-io/browser/blob/main/LICENSE) [![Twitter Follow](https://img.shields.io/twitter/follow/lightpanda_io)](https://twitter.com/lightpanda_io) [![GitHub stars](https://img.shields.io/github/stars/lightpanda-io/browser)](https://github.com/lightpanda-io/browser) +[![Discord](https://img.shields.io/discord/1391984864894521354?style=flat-square&label=discord)](https://discord.gg/K63XeymfB5)
From 1bdf464ef2c228bc53cc2eb52b07a4f56a9f750e Mon Sep 17 00:00:00 2001 From: Navid EMAD Date: Sun, 15 Mar 2026 23:07:23 +0100 Subject: [PATCH 18/24] Fix CDP WebSocket connection dying during complex page navigation The CDP timeout handler in httpLoop had two compounding bugs: 1. Unit mismatch: timestamp(.monotonic) returns seconds, but ms_remaining is in milliseconds. The comparison and subtraction mixed units. 2. Double-counting: In the .done branch, elapsed was computed as absolute time since last_message, but last_message was never updated in this branch. Each iteration subtracted the growing total elapsed seconds from an already-decremented ms_remaining. During complex page loads, Session._wait() returns .done rapidly (due to JS macrotask execution, background tasks, or errors). Each rapid .done return subtracted the growing elapsed (seconds) from ms_remaining (milliseconds), draining it to zero in ~2 seconds instead of the configured 10-second timeout. Fix: use milliTimestamp() for consistent units, update last_message in the .done branch for incremental elapsed tracking, and use >= for correct boundary comparison. Fixes #1849 Co-Authored-By: Claude Opus 4.6 --- src/Server.zig | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Server.zig b/src/Server.zig index 23ddefb5..f899d43c 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -295,7 +295,7 @@ pub const Client = struct { } var cdp = &self.mode.cdp; - var last_message = timestamp(.monotonic); + var last_message = milliTimestamp(.monotonic); var ms_remaining = self.ws.timeout_ms; while (true) { @@ -304,7 +304,7 @@ pub const Client = struct { if (self.readSocket() == false) { return; } - last_message = timestamp(.monotonic); + last_message = milliTimestamp(.monotonic); ms_remaining = self.ws.timeout_ms; }, .no_page => { @@ -319,16 +319,18 @@ pub const Client = struct { if (self.readSocket() == false) { return; } - last_message = timestamp(.monotonic); + last_message = milliTimestamp(.monotonic); ms_remaining = self.ws.timeout_ms; }, .done => { - const elapsed = timestamp(.monotonic) - last_message; - if (elapsed > ms_remaining) { + const now = milliTimestamp(.monotonic); + const elapsed = now - last_message; + if (elapsed >= ms_remaining) { log.info(.app, "CDP timeout", .{}); return; } ms_remaining -= @intCast(elapsed); + last_message = now; }, } } @@ -501,6 +503,7 @@ fn buildJSONVersionResponse( } pub const timestamp = @import("datetime.zig").timestamp; +pub const milliTimestamp = @import("datetime.zig").milliTimestamp; const testing = std.testing; test "server: buildJSONVersionResponse" { From a9d044ec10344dc6d1ae93b5cbec37c21696185a Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 16 Mar 2026 07:11:06 +0800 Subject: [PATCH 19/24] revert domparser test change that belongs to a different PR --- src/browser/tests/domparser.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/tests/domparser.html b/src/browser/tests/domparser.html index d8399055..7930ec87 100644 --- a/src/browser/tests/domparser.html +++ b/src/browser/tests/domparser.html @@ -11,9 +11,9 @@ } { + // Empty XML is a parse error (no root element) const parser = new DOMParser(); - let d = parser.parseFromString('', 'text/xml'); - testing.expectEqual('error', new XMLSerializer().serializeToString(d)); + testing.expectError('Error', () => parser.parseFromString('', 'text/xml')); } } From 0eb43fb53062bc20521e2dcadc7328fae665712c Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 16 Mar 2026 07:16:27 +0800 Subject: [PATCH 20/24] Fix test Fixes test associated with https://github.com/lightpanda-io/browser/pull/1827 --- src/browser/tests/domparser.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/tests/domparser.html b/src/browser/tests/domparser.html index 7930ec87..d8399055 100644 --- a/src/browser/tests/domparser.html +++ b/src/browser/tests/domparser.html @@ -11,9 +11,9 @@ } { - // Empty XML is a parse error (no root element) const parser = new DOMParser(); - testing.expectError('Error', () => parser.parseFromString('', 'text/xml')); + let d = parser.parseFromString('', 'text/xml'); + testing.expectEqual('error', new XMLSerializer().serializeToString(d)); } } From 1121bed49ba16a778a4b807610b379bef0d95b1b Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 16 Mar 2026 07:20:57 +0800 Subject: [PATCH 21/24] remove test that I guess isn't reliable (CI?) --- src/browser/tests/net/fetch.html | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/browser/tests/net/fetch.html b/src/browser/tests/net/fetch.html index c284e6a8..a545a452 100644 --- a/src/browser/tests/net/fetch.html +++ b/src/browser/tests/net/fetch.html @@ -203,15 +203,3 @@ testing.expectEqual(true, response.body !== null); }); - - From 1ec3e156fbe00248e6a611bd9f906e215f4df411 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 16 Mar 2026 07:28:14 +0800 Subject: [PATCH 22/24] Fix partitionKey ignore PR Fixes https://github.com/lightpanda-io/browser/pull/1821 so that it compiles --- src/cdp/domains/network.zig | 2 +- src/cdp/domains/storage.zig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index 5c422a8d..3599a88d 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -122,7 +122,7 @@ fn deleteCookies(cmd: anytype) !void { // This allows Puppeteer's page.setCookie() to work, which sends deleteCookies // with partitionKey as part of its cookie-setting workflow. if (params.partitionKey != null) { - log.debug(.network, "partitionKey ignored in deleteCookies", .{}); + log.warn(.not_implemented, "partition key", .{.src = "deleteCookies"}); } const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig index 497f7efa..92a8018a 100644 --- a/src/cdp/domains/storage.zig +++ b/src/cdp/domains/storage.zig @@ -132,7 +132,7 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void { // This allows Puppeteer's page.setCookie() to work, which may send cookies with // partitionKey as part of its cookie-setting workflow. if (param.partitionKey != null) { - log.debug(.storage, "partitionKey ignored in setCdpCookie", .{}); + log.warn(.not_implemented, "partition key", .{.src = "setCdpCookie"}); } // Still reject unsupported features if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null) { From 323ec0046c584aefa63069135612554e5dabc85f Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 16 Mar 2026 07:36:14 +0800 Subject: [PATCH 23/24] zig fmt --- src/cdp/domains/network.zig | 2 +- src/cdp/domains/storage.zig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index 3599a88d..5b9a49df 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -122,7 +122,7 @@ fn deleteCookies(cmd: anytype) !void { // This allows Puppeteer's page.setCookie() to work, which sends deleteCookies // with partitionKey as part of its cookie-setting workflow. if (params.partitionKey != null) { - log.warn(.not_implemented, "partition key", .{.src = "deleteCookies"}); + log.warn(.not_implemented, "partition key", .{ .src = "deleteCookies" }); } const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig index 92a8018a..e0f67456 100644 --- a/src/cdp/domains/storage.zig +++ b/src/cdp/domains/storage.zig @@ -132,7 +132,7 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void { // This allows Puppeteer's page.setCookie() to work, which may send cookies with // partitionKey as part of its cookie-setting workflow. if (param.partitionKey != null) { - log.warn(.not_implemented, "partition key", .{.src = "setCdpCookie"}); + log.warn(.not_implemented, "partition key", .{ .src = "setCdpCookie" }); } // Still reject unsupported features if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null) { From 21fc6d1cf6d2a701d7abde317a1accbc97e5233f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Mon, 16 Mar 2026 09:41:28 +0900 Subject: [PATCH 24/24] cdp: explain buffer size for int serialization --- src/cdp/AXNode.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cdp/AXNode.zig b/src/cdp/AXNode.zig index ca1c1b98..41a8e085 100644 --- a/src/cdp/AXNode.zig +++ b/src/cdp/AXNode.zig @@ -230,6 +230,7 @@ pub const Writer = struct { switch (value) { .integer => |v| { // CDP spec requires integer values to be serialized as strings. + // 20 bytes is enough for the decimal representation of a 64-bit integer. var buf: [20]u8 = undefined; const s = try std.fmt.bufPrint(&buf, "{d}", .{v}); try w.write(s);