From adb4551362cb8310192f589c7eda927f0a2d344c Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Tue, 25 Aug 2026 20:52:43 -0700 Subject: [PATCH 1/3] support Blob as XHR responseType --- src/browser/webapi/net/XMLHttpRequest.zig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 4e7b06f7e..87da459c9 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -31,6 +31,7 @@ const Frame = @import("../../Frame.zig"); const Node = @import("../Node.zig"); const Event = @import("../Event.zig"); const EventTarget = @import("../EventTarget.zig"); +const Blob = @import("../Blob.zig"); const Headers = @import("Headers.zig"); const BodyInit = @import("body_init.zig").BodyInit; @@ -91,6 +92,7 @@ const Response = union(enum) { json: js.Value.Global, document: *Node.Document, arraybuffer: js.ArrayBuffer, + blob: *Blob, }; const ResponseType = enum { @@ -99,6 +101,7 @@ const ResponseType = enum { json, document, arraybuffer, + blob, pub fn toString(self: ResponseType) []const u8 { return switch (self) { @@ -526,6 +529,12 @@ pub fn getResponse(self: *XMLHttpRequest, exec: *const Execution) !?Response { } }, .arraybuffer => .{ .arraybuffer = .{ .values = data } }, + .blob => blk: { + const mime = self._override_mime orelse self._response_mime; + const content_type = if (mime) |m| m.contentTypeString() else ""; + const blob = try Blob.initFromBytes(data, content_type, exec); + break :blk .{ .blob = blob }; + }, }; self._response = res; From 3d42ffa0f88ed1d9b5858ae257fcc2820c34afd2 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 26 Aug 2026 08:11:29 -0700 Subject: [PATCH 2/3] properly ref count blob in XHR response --- src/browser/webapi/net/XMLHttpRequest.zig | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 87da459c9..144603867 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -123,12 +123,23 @@ pub fn init(exec: *const Execution) !*XMLHttpRequest { return self; } -pub fn deinit(self: *XMLHttpRequest, _: *Page) void { +fn clearResponse(self: *XMLHttpRequest, page: *Page) void { + if (self._response) |res| { + if (res == .blob) { + res.blob.releaseRef(page); + } + } + self._response = null; +} + +pub fn deinit(self: *XMLHttpRequest, page: *Page) void { if (self._http_transfer) |resp| { resp.abort(error.Abort); self._http_transfer = null; } + self.clearResponse(page); + if (self._on_ready_state_change) |func| { func.release(); } @@ -211,7 +222,7 @@ pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8, async // Reset internal state. _override_mime intentionally survives open() // per https://xhr.spec.whatwg.org/#the-overridemimetype()-method. - self._response = null; + self.clearResponse(self._exec.page); self._response_xml = null; self._response_data.clearRetainingCapacity(); self._response_status = 0; @@ -533,6 +544,7 @@ pub fn getResponse(self: *XMLHttpRequest, exec: *const Execution) !?Response { const mime = self._override_mime orelse self._response_mime; const content_type = if (mime) |m| m.contentTypeString() else ""; const blob = try Blob.initFromBytes(data, content_type, exec); + blob.acquireRef(); break :blk .{ .blob = blob }; }, }; From abf5bdecb6793a55e1c7fd10c1a1c6e769ee84e3 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 27 Aug 2026 09:42:02 +0800 Subject: [PATCH 3/3] mem: Use new clearResponse() to eagerly free XHR JSON response And, add blob tests --- src/browser/tests/net/xhr.html | 77 +++++++++++++++++++++++ src/browser/webapi/net/XMLHttpRequest.zig | 14 +++-- 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/src/browser/tests/net/xhr.html b/src/browser/tests/net/xhr.html index 3698db699..0e0a7789f 100644 --- a/src/browser/tests/net/xhr.html +++ b/src/browser/tests/net/xhr.html @@ -642,3 +642,80 @@ handler runs, abort() cancels the re-send and no load is delivered. }); } + + + + + + diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 144603867..f27db7971 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -28,10 +28,10 @@ const Mime = @import("../../Mime.zig"); const Page = @import("../../Page.zig"); const Frame = @import("../../Frame.zig"); +const Blob = @import("../Blob.zig"); const Node = @import("../Node.zig"); const Event = @import("../Event.zig"); const EventTarget = @import("../EventTarget.zig"); -const Blob = @import("../Blob.zig"); const Headers = @import("Headers.zig"); const BodyInit = @import("body_init.zig").BodyInit; @@ -125,11 +125,13 @@ pub fn init(exec: *const Execution) !*XMLHttpRequest { fn clearResponse(self: *XMLHttpRequest, page: *Page) void { if (self._response) |res| { - if (res == .blob) { - res.blob.releaseRef(page); + switch (res) { + .blob => |b| b.releaseRef(page), + .json => |js_val| js_val.release(), + else => {}, } + self._response = null; } - self._response = null; } pub fn deinit(self: *XMLHttpRequest, page: *Page) void { @@ -578,7 +580,9 @@ pub fn getResponseXML(self: *XMLHttpRequest, exec: *const Execution) !?*Node.Doc // With responseType "", only an XML final MIME type is parsed (an HTML // one yields null); absent a Content-Type it defaults to text/xml. const final: Mime = self._override_mime orelse self._response_mime orelse .{ .content_type = .text_xml }; - if (!final.isXML()) return null; + if (!final.isXML()) { + return null; + } switch (exec.js.global) { .frame => |frame| {