From 503ca4ce077d279da0d2c52577ae1f3c43d9829c Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:14:22 -0700 Subject: [PATCH 1/3] feat: enrich CDP /json/version and add /json/list endpoint Add Browser, Protocol-Version, and User-Agent fields to the /json/version CDP endpoint response. Previously it only returned webSocketDebuggerUrl, while Chrome and other CDP browsers return 7+ fields that automation tools use for capability detection. Also add /json/list and /json endpoints that return an empty JSON array, matching the standard CDP endpoint layout that tools like Puppeteer and chromedp expect. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Server.zig | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/src/Server.zig b/src/Server.zig index 6800ddc9..5f0883ad 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -444,6 +444,14 @@ pub const Client = struct { return false; } + if (std.mem.eql(u8, url, "/json/list") or std.mem.eql(u8, url, "/json/list/") or + std.mem.eql(u8, url, "/json") or std.mem.eql(u8, url, "/json/")) + { + try self.ws.send(empty_json_list_response); + self.ws.shutdown(); + return false; + } + return error.NotFound; } @@ -486,8 +494,15 @@ fn buildJSONVersionResponse( .message = "when --host is set to 0.0.0.0 consider setting --advertise-host to a reachable address", }); } - const body_format = "{{\"webSocketDebuggerUrl\": \"ws://{s}:{d}/\"}}"; - const body_len = std.fmt.count(body_format, .{ host, port }); + const version = lp.build_config.version; + const body_format = + "{{" ++ + "\"Browser\": \"Lightpanda/{s}\", " ++ + "\"Protocol-Version\": \"1.3\", " ++ + "\"User-Agent\": \"Lightpanda/{s}\", " ++ + "\"webSocketDebuggerUrl\": \"ws://{s}:{d}/\"" ++ + "}}"; + const body_len = std.fmt.count(body_format, .{ version, version, host, port }); // We send a Connection: Close (and actually close the connection) // because chromedp (Go driver) sends a request to /json/version and then @@ -501,9 +516,16 @@ fn buildJSONVersionResponse( "Connection: Close\r\n" ++ "Content-Type: application/json; charset=UTF-8\r\n\r\n" ++ body_format; - return try std.fmt.allocPrint(app.allocator, response_format, .{ body_len, host, port }); + return try std.fmt.allocPrint(app.allocator, response_format, .{ body_len, version, version, host, port }); } +const empty_json_list_response = + "HTTP/1.1 200 OK\r\n" ++ + "Content-Length: 2\r\n" ++ + "Connection: Close\r\n" ++ + "Content-Type: application/json; charset=UTF-8\r\n\r\n" ++ + "[]"; + pub const timestamp = @import("datetime.zig").timestamp; pub const milliTimestamp = @import("datetime.zig").milliTimestamp; @@ -512,11 +534,16 @@ test "server: buildJSONVersionResponse" { const res = try buildJSONVersionResponse(testing.test_app); defer testing.test_app.allocator.free(res); - try testing.expectEqual("HTTP/1.1 200 OK\r\n" ++ - "Content-Length: 48\r\n" ++ - "Connection: Close\r\n" ++ - "Content-Type: application/json; charset=UTF-8\r\n\r\n" ++ - "{\"webSocketDebuggerUrl\": \"ws://127.0.0.1:9222/\"}", res); + // The response includes the build version, so check structure rather than exact bytes. + try testing.expect(std.mem.startsWith(u8, res, "HTTP/1.1 200 OK\r\n")); + try testing.expect(std.mem.indexOf(u8, res, "Content-Type: application/json") != null); + try testing.expect(std.mem.indexOf(u8, res, "Connection: Close") != null); + + // Verify all required JSON fields are present in the body + try testing.expect(std.mem.indexOf(u8, res, "\"Browser\": \"Lightpanda/") != null); + try testing.expect(std.mem.indexOf(u8, res, "\"Protocol-Version\": \"1.3\"") != null); + try testing.expect(std.mem.indexOf(u8, res, "\"User-Agent\": \"Lightpanda/") != null); + try testing.expect(std.mem.indexOf(u8, res, "\"webSocketDebuggerUrl\": \"ws://127.0.0.1:9222/\"") != null); } test "Client: http invalid request" { From 416984d32fcb035d546fc7d4e28ea4dca91f36c1 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 9 Apr 2026 20:48:10 -0700 Subject: [PATCH 2/3] fix: update integration test for enriched /json/version response The integration test at "server: get /json/version" was hardcoding the old response with Content-Length: 48. Updated to verify the enriched fields structurally since the version string varies at build time. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Server.zig | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Server.zig b/src/Server.zig index 5f0883ad..68e3a53f 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -755,20 +755,16 @@ test "server: 404" { } test "server: get /json/version" { - const expected_response = - "HTTP/1.1 200 OK\r\n" ++ - "Content-Length: 48\r\n" ++ - "Connection: Close\r\n" ++ - "Content-Type: application/json; charset=UTF-8\r\n\r\n" ++ - "{\"webSocketDebuggerUrl\": \"ws://127.0.0.1:9222/\"}"; - { // twice on the same connection var c = try createTestClient(); defer c.deinit(); const res1 = try c.httpRequest("GET /json/version HTTP/1.1\r\n\r\n"); - try testing.expectEqual(expected_response, res1); + try testing.expect(std.mem.startsWith(u8, res1, "HTTP/1.1 200 OK\r\n")); + try testing.expect(std.mem.indexOf(u8, res1, "\"Browser\": \"Lightpanda/") != null); + try testing.expect(std.mem.indexOf(u8, res1, "\"Protocol-Version\": \"1.3\"") != null); + try testing.expect(std.mem.indexOf(u8, res1, "\"webSocketDebuggerUrl\": \"ws://127.0.0.1:9222/\"") != null); } { @@ -777,7 +773,8 @@ test "server: get /json/version" { defer c.deinit(); const res1 = try c.httpRequest("GET /json/version HTTP/1.1\r\n\r\n"); - try testing.expectEqual(expected_response, res1); + try testing.expect(std.mem.startsWith(u8, res1, "HTTP/1.1 200 OK\r\n")); + try testing.expect(std.mem.indexOf(u8, res1, "\"Browser\": \"Lightpanda/") != null); } } From 224a7333f2bfb9ad9bb58cfde00dd028c38a1edd Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:52:03 -0700 Subject: [PATCH 3/3] fix: use fixed Lightpanda/1.0 for /json/version User-Agent Replace dynamic build version string with stable Lightpanda/1.0 in the Browser and User-Agent fields of the /json/version response. The dev version (1.0.0-dev.5492+...) is not useful for CDP clients. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Server.zig | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Server.zig b/src/Server.zig index 68e3a53f..e3edda06 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -494,15 +494,14 @@ fn buildJSONVersionResponse( .message = "when --host is set to 0.0.0.0 consider setting --advertise-host to a reachable address", }); } - const version = lp.build_config.version; const body_format = "{{" ++ - "\"Browser\": \"Lightpanda/{s}\", " ++ + "\"Browser\": \"Lightpanda/1.0\", " ++ "\"Protocol-Version\": \"1.3\", " ++ - "\"User-Agent\": \"Lightpanda/{s}\", " ++ + "\"User-Agent\": \"Lightpanda/1.0\", " ++ "\"webSocketDebuggerUrl\": \"ws://{s}:{d}/\"" ++ "}}"; - const body_len = std.fmt.count(body_format, .{ version, version, host, port }); + const body_len = std.fmt.count(body_format, .{ host, port }); // We send a Connection: Close (and actually close the connection) // because chromedp (Go driver) sends a request to /json/version and then @@ -516,7 +515,7 @@ fn buildJSONVersionResponse( "Connection: Close\r\n" ++ "Content-Type: application/json; charset=UTF-8\r\n\r\n" ++ body_format; - return try std.fmt.allocPrint(app.allocator, response_format, .{ body_len, version, version, host, port }); + return try std.fmt.allocPrint(app.allocator, response_format, .{ body_len, host, port }); } const empty_json_list_response =