Merge pull request #3284 from lightpanda-io/xhr-blob

XHR Blob Response Type
This commit is contained in:
Karl Seguin authored and GitHub committed 2026-08-27 10:44:05 +08:00
commit a7bda0ea57
2 files changed
+105 -3

No files matched your search

+77
View File
@@ -642,3 +642,80 @@ handler runs, abort() cancels the re-send and no load is delivered.
});
}
</script>
<script id=xhr_blob type=module>
{
const state = await testing.async();
const req = new XMLHttpRequest();
req.onload = () => { state.resolve() };
req.open('GET', 'http://127.0.0.1:9582/xhr/binary');
req.responseType = 'blob';
req.send();
await state.done(() => {
testing.expectEqual(200, req.status);
testing.expectEqual('blob', req.responseType);
testing.expectEqual(true, req.response instanceof Blob);
testing.expectEqual(7, req.response.size);
// Per spec the blob response entity body is cached: the same object comes
// back on every access.
testing.expectEqual(req.response, req.response);
// TODO: should be 'application/octet-stream'. Mime keeps a content-type
// enum, not the header text, and Mime.contentTypeString() has no entry
// for it, so the type is lost on the way to the Blob.
testing.expectEqual('', req.response.type);
});
testing.expectEqual([0, 0, 1, 2, 0, 0, 9], new Int8Array(await req.response.arrayBuffer()));
}
</script>
<script id=xhr_blob_reopen type=module>
{
// open() discards the cached response, dropping the XHR's reference to the
// blob. A blob the script is still holding has to survive that: the cache
// and the JS wrapper each own a reference. Release without a matching
// acquire (or a double release) frees the arena out from under `first`.
const state = await testing.async();
const req = new XMLHttpRequest();
req.onload = () => { state.resolve() };
req.open('GET', 'http://127.0.0.1:9582/xhr/binary');
req.responseType = 'blob';
req.send();
await state.done(() => testing.expectEqual(200, req.status));
const first = req.response;
const state2 = await testing.async();
req.onload = () => { state2.resolve() };
req.open('GET', 'http://127.0.0.1:9582/xhr/binary');
req.send();
await state2.done(() => {
testing.expectEqual(true, req.response instanceof Blob);
// the second request parses a fresh blob, not the discarded one
testing.expectEqual(false, req.response === first);
});
testing.expectEqual(7, first.size);
testing.expectEqual([0, 0, 1, 2, 0, 0, 9], new Int8Array(await first.arrayBuffer()));
}
</script>
<script id=xhr_json_reopen type=module>
{
// open() releases the persisted handle on the cached json response. That
// handle is one v8 root, not ownership of the value, so a script holding
// the parsed object must still be able to read it afterwards.
const state = await testing.async();
const req = new XMLHttpRequest();
req.onload = () => { state.resolve() };
req.open('GET', 'http://127.0.0.1:9582/xhr/json');
req.responseType = 'json';
req.send();
await state.done(() => testing.expectEqual('9000!!!', req.response.over));
const first = req.response;
req.open('GET', 'http://127.0.0.1:9582/xhr/json');
testing.expectEqual('9000!!!', first.over);
}
</script>
+28 -3
View File
@@ -28,6 +28,7 @@ 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");
@@ -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) {
@@ -120,12 +123,25 @@ 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| {
switch (res) {
.blob => |b| b.releaseRef(page),
.json => |js_val| js_val.release(),
else => {},
}
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();
}
@@ -208,7 +224,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;
@@ -526,6 +542,13 @@ 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);
blob.acquireRef();
break :blk .{ .blob = blob };
},
};
self._response = res;
@@ -557,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| {