From 66342b35db10a26bb40cf1be0cbafafa69cdc774 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 24 Dec 2025 15:35:03 +0100 Subject: [PATCH 1/4] add test for big json number with fetch/xhr --- src/main.zig | 2 +- src/tests/fetch/fetch.html | 6 ++++-- src/tests/xhr/xhr.html | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main.zig b/src/main.zig index f3f9ec65..cd3be8f3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -862,7 +862,7 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void { } if (std.mem.eql(u8, path, "/xhr/json")) { - return req.respond("{\"over\":\"9000!!!\"}", .{ + return req.respond("{\"over\":\"9000!!!\",\"updated_at\":1765867200000}", .{ .extra_headers = &.{ .{ .name = "Content-Type", .value = "application/json" }, }, diff --git a/src/tests/fetch/fetch.html b/src/tests/fetch/fetch.html index 877f887b..5d25ca54 100644 --- a/src/tests/fetch/fetch.html +++ b/src/tests/fetch/fetch.html @@ -12,7 +12,8 @@ }); testing.async(promise1, (json) => { - testing.expectEqual({over: '9000!!!'}, json); + testing.expectEqual("number", typeof json.updated_at); + testing.expectEqual({over: '9000!!!',updated_at:1765867200000}, json); }); @@ -29,6 +30,7 @@ }); testing.async(promise1, (json) => { - testing.expectEqual({over: '9000!!!'}, json); + testing.expectEqual("number", typeof json.updated_at); + testing.expectEqual({over: '9000!!!',updated_at:1765867200000}, json); }); diff --git a/src/tests/xhr/xhr.html b/src/tests/xhr/xhr.html index 13ab6216..7afd28c2 100644 --- a/src/tests/xhr/xhr.html +++ b/src/tests/xhr/xhr.html @@ -65,6 +65,8 @@ testing.expectEqual(200, req3.status); testing.expectEqual('OK', req3.statusText); testing.expectEqual('9000!!!', req3.response.over); + testing.expectEqual("number", typeof req3.response.updated_at); + testing.expectEqual(1765867200000, req3.response.updated_at); }); From 1dcccef0800743fcc430212f2175bd1e17cb8221 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 24 Dec 2025 15:19:41 +0100 Subject: [PATCH 2/4] use V8 json parser with xhr/fetch webAPIs The pure zig JSON parser didn't generate the same type of values than JS JSON.parse command. Using directly V8's JSON parser gives the assurance to have the right JS types. Moreover, it avoid data transformations between Zig and V8. --- src/browser/fetch/Request.zig | 9 ++------- src/browser/fetch/Response.zig | 9 ++------- src/browser/xhr/xhr.zig | 25 ++++++++++--------------- 3 files changed, 14 insertions(+), 29 deletions(-) diff --git a/src/browser/fetch/Request.zig b/src/browser/fetch/Request.zig index f13a8cb8..e22d9f77 100644 --- a/src/browser/fetch/Request.zig +++ b/src/browser/fetch/Request.zig @@ -254,17 +254,12 @@ pub fn _json(self: *Response, page: *Page) !js.Promise { self.body_used = true; if (self.body) |body| { - const p = std.json.parseFromSliceLeaky( - std.json.Value, - page.call_arena, - body, - .{}, - ) catch |e| { + const value = js.Value.fromJson(page.js, body) catch |e| { log.info(.browser, "invalid json", .{ .err = e, .source = "Request" }); return error.SyntaxError; }; - return page.js.resolvePromise(p); + return page.js.resolvePromise(value); } return page.js.resolvePromise(null); } diff --git a/src/browser/fetch/Response.zig b/src/browser/fetch/Response.zig index 69f1c39e..dc83071c 100644 --- a/src/browser/fetch/Response.zig +++ b/src/browser/fetch/Response.zig @@ -179,17 +179,12 @@ pub fn _json(self: *Response, page: *Page) !js.Promise { if (self.body) |body| { self.body_used = true; - const p = std.json.parseFromSliceLeaky( - std.json.Value, - page.call_arena, - body, - .{}, - ) catch |e| { + const value = js.Value.fromJson(page.js, body) catch |e| { log.info(.browser, "invalid json", .{ .err = e, .source = "Response" }); return error.SyntaxError; }; - return page.js.resolvePromise(p); + return page.js.resolvePromise(value); } return page.js.resolvePromise(null); } diff --git a/src/browser/xhr/xhr.zig b/src/browser/xhr/xhr.zig index dfc9ca99..588c103e 100644 --- a/src/browser/xhr/xhr.zig +++ b/src/browser/xhr/xhr.zig @@ -31,6 +31,7 @@ const Mime = @import("../mime.zig").Mime; const parser = @import("../netsurf.zig"); const Page = @import("../page.zig").Page; const Http = @import("../../http/Http.zig"); +const js = @import("../js/js.zig"); // XHR interfaces // https://xhr.spec.whatwg.org/#interface-xmlhttprequest @@ -128,21 +129,19 @@ pub const XMLHttpRequest = struct { JSON, }; - const JSONValue = std.json.Value; - const Response = union(ResponseType) { Empty: void, Text: []const u8, ArrayBuffer: void, Blob: void, Document: *parser.Document, - JSON: JSONValue, + JSON: js.Value, }; const ResponseObj = union(enum) { Document: *parser.Document, Failure: void, - JSON: JSONValue, + JSON: js.Value, fn deinit(self: ResponseObj) void { switch (self) { @@ -605,7 +604,7 @@ pub const XMLHttpRequest = struct { } // https://xhr.spec.whatwg.org/#the-response-attribute - pub fn get_response(self: *XMLHttpRequest) !?Response { + pub fn get_response(self: *XMLHttpRequest, page: *Page) !?Response { if (self.response_type == .Empty or self.response_type == .Text) { if (self.state == .loading or self.state == .done) { return .{ .Text = try self.get_responseText() }; @@ -652,7 +651,7 @@ pub const XMLHttpRequest = struct { // TODO Let jsonObject be the result of running parse JSON from bytes // on this’s received bytes. If that threw an exception, then return // null. - self.setResponseObjJSON(); + self.setResponseObjJSON(page); } if (self.response_obj) |obj| { @@ -691,22 +690,18 @@ pub const XMLHttpRequest = struct { }; } - // setResponseObjJSON parses the received bytes as a std.json.Value. - fn setResponseObjJSON(self: *XMLHttpRequest) void { - // TODO should we use parseFromSliceLeaky if we expect the allocator is - // already an arena? - const p = std.json.parseFromSliceLeaky( - JSONValue, - self.arena, + // setResponseObjJSON parses the received bytes as a js.Value. + fn setResponseObjJSON(self: *XMLHttpRequest, page: *Page) void { + const value = js.Value.fromJson( + page.js, self.response_bytes.items, - .{}, ) catch |e| { log.warn(.http, "invalid json", .{ .err = e, .url = self.url, .source = "xhr" }); self.response_obj = .{ .Failure = {} }; return; }; - self.response_obj = .{ .JSON = p }; + self.response_obj = .{ .JSON = value }; } pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 { From beef458c3c8df4448a7a99a879dc1fe37093f9e1 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 24 Dec 2025 16:36:24 +0100 Subject: [PATCH 3/4] js: persist value returned by v8 JSON parser --- src/browser/js/Context.zig | 8 ++++++++ src/browser/js/js.zig | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 6ac0dae0..2e577265 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -14,6 +14,7 @@ const types = @import("types.zig"); const Caller = @import("Caller.zig"); const NamedFunction = Caller.NamedFunction; const PersistentObject = v8.Persistent(v8.Object); +const PersistentValue = v8.Persistent(v8.Value); const PersistentModule = v8.Persistent(v8.Module); const PersistentPromise = v8.Persistent(v8.Promise); const PersistentFunction = v8.Persistent(v8.Function); @@ -70,6 +71,9 @@ identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty, // we now simply persist every time persist() is called. js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty, +// js_value_list tracks persisted js values. +js_value_list: std.ArrayListUnmanaged(PersistentValue) = .empty, + // Various web APIs depend on having a persistent promise resolver. They // require for this PromiseResolver to be valid for a lifetime longer than // the function that resolves/rejects them. @@ -149,6 +153,10 @@ pub fn deinit(self: *Context) void { p.deinit(); } + for (self.js_value_list.items) |*p| { + p.deinit(); + } + for (self.persisted_promise_resolvers.items) |*p| { p.deinit(); } diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 1504e846..a6af906f 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -148,6 +148,8 @@ pub const Exception = struct { }; pub const Value = struct { + const PersistentValue = v8.Persistent(v8.Value); + value: v8.Value, context: *const Context, @@ -159,7 +161,11 @@ pub const Value = struct { pub fn fromJson(ctx: *Context, json: []const u8) !Value { const json_string = v8.String.initUtf8(ctx.isolate, json); const value = try v8.Json.parse(ctx.v8_context, json_string); - return Value{ .context = ctx, .value = value }; + + const persisted = PersistentValue.init(ctx.isolate, value); + try ctx.js_value_list.append(ctx.arena, persisted); + + return Value{ .context = ctx, .value = persisted.toValue() }; } }; From 5e4e4dcbc61d99202e708be13d525bee6725a780 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 25 Dec 2025 12:07:32 +0100 Subject: [PATCH 4/4] split Value.fromJson into Value.persist --- src/browser/fetch/Request.zig | 3 ++- src/browser/fetch/Response.zig | 3 ++- src/browser/js/js.zig | 11 ++++++++--- src/browser/xhr/xhr.zig | 8 +++++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/browser/fetch/Request.zig b/src/browser/fetch/Request.zig index e22d9f77..6e09a5e3 100644 --- a/src/browser/fetch/Request.zig +++ b/src/browser/fetch/Request.zig @@ -258,8 +258,9 @@ pub fn _json(self: *Response, page: *Page) !js.Promise { log.info(.browser, "invalid json", .{ .err = e, .source = "Request" }); return error.SyntaxError; }; + const pvalue = try value.persist(page.js); - return page.js.resolvePromise(value); + return page.js.resolvePromise(pvalue); } return page.js.resolvePromise(null); } diff --git a/src/browser/fetch/Response.zig b/src/browser/fetch/Response.zig index dc83071c..4932b45e 100644 --- a/src/browser/fetch/Response.zig +++ b/src/browser/fetch/Response.zig @@ -183,8 +183,9 @@ pub fn _json(self: *Response, page: *Page) !js.Promise { log.info(.browser, "invalid json", .{ .err = e, .source = "Response" }); return error.SyntaxError; }; + const pvalue = try value.persist(page.js); - return page.js.resolvePromise(value); + return page.js.resolvePromise(pvalue); } return page.js.resolvePromise(null); } diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index a6af906f..5a35b100 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -161,11 +161,16 @@ pub const Value = struct { pub fn fromJson(ctx: *Context, json: []const u8) !Value { const json_string = v8.String.initUtf8(ctx.isolate, json); const value = try v8.Json.parse(ctx.v8_context, json_string); + return Value{ .context = ctx, .value = value }; + } - const persisted = PersistentValue.init(ctx.isolate, value); - try ctx.js_value_list.append(ctx.arena, persisted); + pub fn persist(self: Value, context: *Context) !Value { + const js_value = self.value; - return Value{ .context = ctx, .value = persisted.toValue() }; + const persisted = PersistentValue.init(context.isolate, js_value); + try context.js_value_list.append(context.arena, persisted); + + return Value{ .context = context, .value = persisted.toValue() }; } }; diff --git a/src/browser/xhr/xhr.zig b/src/browser/xhr/xhr.zig index 588c103e..2207a897 100644 --- a/src/browser/xhr/xhr.zig +++ b/src/browser/xhr/xhr.zig @@ -701,7 +701,13 @@ pub const XMLHttpRequest = struct { return; }; - self.response_obj = .{ .JSON = value }; + const pvalue = value.persist(page.js) catch |e| { + log.warn(.http, "persist v8 json value", .{ .err = e, .url = self.url, .source = "xhr" }); + self.response_obj = .{ .Failure = {} }; + return; + }; + + self.response_obj = .{ .JSON = pvalue }; } pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 {