From 5d2801c6521a034425b0b653ae03db7f4b5102f4 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 17 Mar 2026 10:31:32 +0800 Subject: [PATCH] Support blob urls in XHR and Fetch Used quite a bit in WPT. Not sure how common this is in real world though. --- src/browser/Page.zig | 20 ++++++------ src/browser/tests/net/fetch.html | 22 +++++++++++++ src/browser/tests/net/xhr.html | 23 +++++++++++++ src/browser/webapi/net/Fetch.zig | 29 +++++++++++++++-- src/browser/webapi/net/XMLHttpRequest.zig | 39 +++++++++++++++++++++++ 5 files changed, 121 insertions(+), 12 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index b82ca41d..b973aaa2 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -407,16 +407,9 @@ pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool { return std.mem.startsWith(u8, url, current_origin); } -/// Look up a blob URL in this page's registry, walking up the parent chain. +/// Look up a blob URL in this page's registry. pub fn lookupBlobUrl(self: *Page, url: []const u8) ?*Blob { - var current: ?*Page = self; - while (current) |page| { - if (page._blob_urls.get(url)) |blob| { - return blob; - } - current = page.parent; - } - return null; + return self._blob_urls.get(url); } pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void { @@ -457,7 +450,14 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi // Content injection if (is_blob) { - const blob = self.lookupBlobUrl(request_url) orelse { + // For navigation, walk up the parent chain to find blob URLs + // (e.g., parent creates blob URL and sets iframe.src to it) + const blob = blk: { + var current: ?*Page = self.parent; + while (current) |page| { + if (page._blob_urls.get(request_url)) |b| break :blk b; + current = page.parent; + } log.warn(.js, "invalid blob", .{ .url = request_url }); return error.BlobNotFound; }; diff --git a/src/browser/tests/net/fetch.html b/src/browser/tests/net/fetch.html index a545a452..10ce6677 100644 --- a/src/browser/tests/net/fetch.html +++ b/src/browser/tests/net/fetch.html @@ -203,3 +203,25 @@ testing.expectEqual(true, response.body !== null); }); + + diff --git a/src/browser/tests/net/xhr.html b/src/browser/tests/net/xhr.html index 64fac5c3..a8683142 100644 --- a/src/browser/tests/net/xhr.html +++ b/src/browser/tests/net/xhr.html @@ -283,3 +283,26 @@ testing.expectEqual(XMLHttpRequest.UNSENT, req.readyState); }); + + diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index ab98a8e5..7ab7b8ec 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -25,6 +25,7 @@ const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); const URL = @import("../../URL.zig"); +const Blob = @import("../Blob.zig"); const Request = @import("Request.zig"); const Response = @import("Response.zig"); @@ -44,11 +45,15 @@ pub const InitOpts = Request.InitOpts; pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise { const request = try Request.init(input, options, page); + const resolver = page.js.local.?.createPromiseResolver(); + + if (std.mem.startsWith(u8, request._url, "blob:")) { + return handleBlobUrl(request._url, resolver, page); + } + const response = try Response.init(null, .{ .status = 0 }, page); errdefer response.deinit(true, page._session); - const resolver = page.js.local.?.createPromiseResolver(); - const fetch = try response._arena.create(Fetch); fetch.* = .{ ._page = page, @@ -90,6 +95,26 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise { return resolver.promise(); } +fn handleBlobUrl(url: []const u8, resolver: js.PromiseResolver, page: *Page) !js.Promise { + const blob: *Blob = page.lookupBlobUrl(url) orelse { + resolver.rejectError("fetch blob error", .{ .type_error = "BlobNotFound" }); + return resolver.promise(); + }; + + const response = try Response.init(null, .{ .status = 200 }, page); + response._body = try response._arena.dupe(u8, blob._slice); + response._url = try response._arena.dupeZ(u8, url); + response._type = .basic; + + if (blob._mime.len > 0) { + try response._headers.append("Content-Type", blob._mime, page); + } + + const js_val = try page.js.local.?.zigValueToJs(response, .{}); + resolver.resolve("fetch blob done", js_val); + return resolver.promise(); +} + fn httpStartCallback(transfer: *HttpClient.Transfer) !void { const self: *Fetch = @ptrCast(@alignCast(transfer.ctx)); if (comptime IS_DEBUG) { diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index d8d5e369..399e4217 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -29,6 +29,7 @@ const Page = @import("../../Page.zig"); const Session = @import("../../Session.zig"); const Node = @import("../Node.zig"); +const Blob = @import("../Blob.zig"); const Event = @import("../Event.zig"); const Headers = @import("Headers.zig"); const EventTarget = @import("../EventTarget.zig"); @@ -211,6 +212,11 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void { } const page = self._page; + + if (std.mem.startsWith(u8, self._url, "blob:")) { + return self.handleBlobUrl(page); + } + const http_client = page._session.browser.http_client; var headers = try http_client.newHeaders(); @@ -242,6 +248,39 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void { page.js.strongRef(self); } + +fn handleBlobUrl(self: *XMLHttpRequest, page: *Page) !void { + const blob = page.lookupBlobUrl(self._url) orelse { + self.handleError(error.BlobNotFound); + return; + }; + + self._response_status = 200; + self._response_url = self._url; + + try self._response_data.appendSlice(self._arena, blob._slice); + self._response_len = blob._slice.len; + + try self.stateChanged(.headers_received, page); + try self._proto.dispatch(.load_start, .{ .loaded = 0, .total = self._response_len orelse 0 }, page); + try self.stateChanged(.loading, page); + try self._proto.dispatch(.progress, .{ + .total = self._response_len orelse 0, + .loaded = self._response_data.items.len, + }, page); + try self.stateChanged(.done, page); + + const loaded = self._response_data.items.len; + try self._proto.dispatch(.load, .{ + .total = loaded, + .loaded = loaded, + }, page); + try self._proto.dispatch(.load_end, .{ + .total = loaded, + .loaded = loaded, + }, page); +} + pub fn getReadyState(self: *const XMLHttpRequest) u32 { return @intFromEnum(self._ready_state); }