diff --git a/src/browser/tests/blob.html b/src/browser/tests/blob.html
index 12cd13f5..0cbf8ea5 100644
--- a/src/browser/tests/blob.html
+++ b/src/browser/tests/blob.html
@@ -98,6 +98,64 @@
}
+
+
+
+
+
+
diff --git a/src/browser/tests/net/response.html b/src/browser/tests/net/response.html
index b7c149ba..3b74e72c 100644
--- a/src/browser/tests/net/response.html
+++ b/src/browser/tests/net/response.html
@@ -2,51 +2,113 @@
-
-
diff --git a/src/browser/webapi/Blob.zig b/src/browser/webapi/Blob.zig
index ac31560a..aa955ce5 100644
--- a/src/browser/webapi/Blob.zig
+++ b/src/browser/webapi/Blob.zig
@@ -21,6 +21,7 @@ const Writer = std.Io.Writer;
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
+const Mime = @import("../Mime.zig");
/// https://w3c.github.io/FileAPI/#blob-section
/// https://developer.mozilla.org/en-US/docs/Web/API/Blob
@@ -50,21 +51,50 @@ const InitOptions = struct {
endings: []const u8 = "transparent",
};
-/// Creates a new Blob.
+/// Creates a new Blob (JS constructor).
pub fn init(
maybe_blob_parts: ?[]const []const u8,
maybe_options: ?InitOptions,
page: *Page,
+) !*Blob {
+ return initWithMimeValidation(maybe_blob_parts, maybe_options, false, page);
+}
+
+/// Creates a new Blob with optional MIME validation.
+/// When validate_mime is true, uses full MIME parsing (for Response/Request).
+/// When false, uses simple ASCII validation per FileAPI spec (for Blob constructor).
+pub fn initWithMimeValidation(
+ maybe_blob_parts: ?[]const []const u8,
+ maybe_options: ?InitOptions,
+ validate_mime: bool,
+ page: *Page,
) !*Blob {
const options: InitOptions = maybe_options orelse .{};
- // Setup MIME; This can be any string according to my observations.
+
const mime: []const u8 = blk: {
const t = options.type;
if (t.len == 0) {
break :blk "";
}
- break :blk try page.arena.dupe(u8, t);
+ const buf = try page.arena.dupe(u8, t);
+
+ if (validate_mime) {
+ // Full MIME parsing per MIME sniff spec (for Content-Type headers)
+ _ = Mime.parse(buf) catch break :blk "";
+ } else {
+ // Simple validation per FileAPI spec (for Blob constructor):
+ // - If any char is outside U+0020-U+007E, return empty string
+ // - Otherwise lowercase
+ for (t) |c| {
+ if (c < 0x20 or c > 0x7E) {
+ break :blk "";
+ }
+ }
+ _ = std.ascii.lowerString(buf, buf);
+ }
+
+ break :blk buf;
};
const data = blk: {
diff --git a/src/browser/webapi/net/Request.zig b/src/browser/webapi/net/Request.zig
index 4316ddbb..aa7e0dd7 100644
--- a/src/browser/webapi/net/Request.zig
+++ b/src/browser/webapi/net/Request.zig
@@ -24,6 +24,7 @@ const Http = @import("../../../http/Http.zig");
const URL = @import("../URL.zig");
const Page = @import("../../Page.zig");
const Headers = @import("Headers.zig");
+const Blob = @import("../Blob.zig");
const Allocator = std.mem.Allocator;
const Request = @This();
@@ -153,6 +154,55 @@ pub fn getHeaders(self: *Request, page: *Page) !*Headers {
return headers;
}
+pub fn blob(self: *Request, page: *Page) !js.Promise {
+ const body = self._body orelse "";
+ const headers = try self.getHeaders(page);
+ const content_type = try headers.get("content-type", page) orelse "";
+
+ const b = try Blob.initWithMimeValidation(
+ &.{body},
+ .{ .type = content_type },
+ true,
+ page,
+ );
+
+ return page.js.local.?.resolvePromise(b);
+}
+
+pub fn text(self: *const Request, page: *Page) !js.Promise {
+ const body = self._body orelse "";
+ return page.js.local.?.resolvePromise(body);
+}
+
+pub fn json(self: *const Request, page: *Page) !js.Promise {
+ const body = self._body orelse "";
+ const local = page.js.local.?;
+ const value = local.parseJSON(body) catch |err| {
+ return local.rejectPromise(.{@errorName(err)});
+ };
+ return local.resolvePromise(try value.persist());
+}
+
+pub fn arrayBuffer(self: *const Request, page: *Page) !js.Promise {
+ return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = self._body orelse "" });
+}
+
+pub fn bytes(self: *const Request, page: *Page) !js.Promise {
+ return page.js.local.?.resolvePromise(js.TypedArray(u8){ .values = self._body orelse "" });
+}
+
+pub fn clone(self: *const Request, page: *Page) !*Request {
+ return page._factory.create(Request{
+ ._url = self._url,
+ ._arena = self._arena,
+ ._method = self._method,
+ ._headers = self._headers,
+ ._cache = self._cache,
+ ._credentials = self._credentials,
+ ._body = self._body,
+ });
+}
+
pub const JsApi = struct {
pub const bridge = js.Bridge(Request);
@@ -168,6 +218,12 @@ pub const JsApi = struct {
pub const headers = bridge.accessor(Request.getHeaders, null, .{});
pub const cache = bridge.accessor(Request.getCache, null, .{});
pub const credentials = bridge.accessor(Request.getCredentials, null, .{});
+ pub const blob = bridge.function(Request.blob, .{});
+ pub const text = bridge.function(Request.text, .{});
+ pub const json = bridge.function(Request.json, .{});
+ pub const arrayBuffer = bridge.function(Request.arrayBuffer, .{});
+ pub const bytes = bridge.function(Request.bytes, .{});
+ pub const clone = bridge.function(Request.clone, .{});
};
const testing = @import("../../../testing.zig");
diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig
index ba1a754d..d2c270ce 100644
--- a/src/browser/webapi/net/Response.zig
+++ b/src/browser/webapi/net/Response.zig
@@ -23,6 +23,7 @@ const Http = @import("../../../http/Http.zig");
const Page = @import("../../Page.zig");
const Headers = @import("Headers.zig");
const ReadableStream = @import("../streams/ReadableStream.zig");
+const Blob = @import("../Blob.zig");
const Allocator = std.mem.Allocator;
@@ -147,6 +148,47 @@ pub fn arrayBuffer(self: *const Response, page: *Page) !js.Promise {
return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = self._body orelse "" });
}
+pub fn blob(self: *const Response, page: *Page) !js.Promise {
+ const body = self._body orelse "";
+ const content_type = try self._headers.get("content-type", page) orelse "";
+
+ const b = try Blob.initWithMimeValidation(
+ &.{body},
+ .{ .type = content_type },
+ true,
+ page,
+ );
+
+ return page.js.local.?.resolvePromise(b);
+}
+
+pub fn bytes(self: *const Response, page: *Page) !js.Promise {
+ return page.js.local.?.resolvePromise(js.TypedArray(u8){ .values = self._body orelse "" });
+}
+
+pub fn clone(self: *const Response, page: *Page) !*Response {
+ const arena = try page.getArena(.{ .debug = "Response.clone" });
+ errdefer page.releaseArena(arena);
+
+ const body = if (self._body) |b| try arena.dupe(u8, b) else null;
+ const status_text = try arena.dupe(u8, self._status_text);
+ const url = try arena.dupeZ(u8, self._url);
+
+ const cloned = try arena.create(Response);
+ cloned.* = .{
+ ._arena = arena,
+ ._status = self._status,
+ ._status_text = status_text,
+ ._url = url,
+ ._body = body,
+ ._type = self._type,
+ ._is_redirected = self._is_redirected,
+ ._headers = try Headers.init(.{ .obj = self._headers }, page),
+ ._transfer = null,
+ };
+ return cloned;
+}
+
pub const JsApi = struct {
pub const bridge = js.Bridge(Response);
@@ -170,6 +212,9 @@ pub const JsApi = struct {
pub const url = bridge.accessor(Response.getURL, null, .{});
pub const redirected = bridge.accessor(Response.isRedirected, null, .{});
pub const arrayBuffer = bridge.function(Response.arrayBuffer, .{});
+ pub const blob = bridge.function(Response.blob, .{});
+ pub const bytes = bridge.function(Response.bytes, .{});
+ pub const clone = bridge.function(Response.clone, .{});
};
const testing = @import("../../../testing.zig");