diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index f8bd99da..0c2bea55 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -408,16 +408,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 {
@@ -458,7 +451,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);
}