diff --git a/src/browser/tests/net/fetch.html b/src/browser/tests/net/fetch.html
index 6d902ace5..61bfda7a5 100644
--- a/src/browser/tests/net/fetch.html
+++ b/src/browser/tests/net/fetch.html
@@ -564,3 +564,30 @@
});
}
+
+
diff --git a/src/browser/tests/net/response.html b/src/browser/tests/net/response.html
index 63f3f9353..1e8a470ac 100644
--- a/src/browser/tests/net/response.html
+++ b/src/browser/tests/net/response.html
@@ -429,3 +429,26 @@
testing.expectTrue(threw);
}
+
+
diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig
index aefc66c1d..d7249993b 100644
--- a/src/browser/webapi/net/Fetch.zig
+++ b/src/browser/webapi/net/Fetch.zig
@@ -45,6 +45,7 @@ _owns_response: bool,
_signal: ?*AbortSignal,
_manual_redirect: bool,
_no_cors: bool,
+_null_body: bool,
_sink: Sink,
pub const Input = Request.Input;
@@ -129,6 +130,7 @@ fn submit(request: *Request, body: ?[]const u8, sink: Sink, exec: *const Executi
._signal = request._signal,
._manual_redirect = request._redirect == .manual,
._no_cors = request._mode == .@"no-cors",
+ ._null_body = request._method == .HEAD,
};
if (comptime lp.IS_DEBUG) {
@@ -211,8 +213,13 @@ fn httpHeaderDoneCallback(transfer: *Transfer) !Transfer.HeaderResult {
}
}
+ const status = transfer.responseStatus().?;
+ if (is_opaque or Response.isNullBodyStatus(status) or (self._manual_redirect and HttpClient.isRedirectStatus(status))) {
+ self._null_body = true;
+ }
+
const arena = self._response._arena;
- if (!is_opaque) {
+ if (self._null_body == false) {
try self._buf.ensureTotalCapacityPrecise(arena.allocator(), transfer.bodyLen());
}
@@ -226,8 +233,8 @@ fn httpHeaderDoneCallback(transfer: *Transfer) !Transfer.HeaderResult {
});
}
- res._status = transfer.responseStatus().?;
- res._status_text = std.http.Status.phrase(@enumFromInt(transfer.responseStatus().?)) orelse "";
+ res._status = status;
+ res._status_text = std.http.Status.phrase(@enumFromInt(status)) orelse "";
res._url = try arena.dupeZ(u8, transfer.req.url);
res._is_redirected = transfer.redirectCount().? > 0;
@@ -290,7 +297,7 @@ fn httpDataCallback(transfer: *Transfer, data: []const u8) !void {
}
}
- if (self._no_cors and transfer.client.obey_cors and transfer._cors_cross_origin) {
+ if (self._null_body) {
return;
}
@@ -301,7 +308,7 @@ fn httpDoneCallback(ctx: *anyopaque) !void {
const self: *Fetch = @ptrCast(@alignCast(ctx));
var response = self._response;
response._http_transfer = null;
- response._body = .{ .bytes = self._buf.items };
+ response._body = if (self._null_body) .empty else .{ .bytes = self._buf.items };
log.info(.http, "request complete", .{
.source = "fetch",
diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig
index de209e081..86eff5b44 100644
--- a/src/browser/webapi/net/Response.zig
+++ b/src/browser/webapi/net/Response.zig
@@ -73,6 +73,7 @@ const InitOpts = struct {
pub const BodyInit = body_init.BodyInit;
pub fn init(body_: ?BodyInit, opts_: ?InitOpts, exec: *const Execution) !*Response {
+ try validateInit(opts_ orelse .{}, body_ != null);
const session = exec.session;
const bucket: lp.ArenaPool.BucketSize = blk: {
@@ -90,6 +91,33 @@ pub fn init(body_: ?BodyInit, opts_: ?InitOpts, exec: *const Execution) !*Respon
return initWithArena(arena, body_, opts_, exec);
}
+fn validateInit(opts: InitOpts, has_body: bool) !void {
+ if (opts.status < 200 or opts.status > 599) {
+ return error.RangeError;
+ }
+
+ if (opts.statusText) |status_text| {
+ // reason-phrase: HTAB, SP, VCHAR and obs-text, all within a ByteString
+ var it = (std.unicode.Utf8View.init(status_text) catch return error.TypeError).iterator();
+ while (it.nextCodepoint()) |cp| switch (cp) {
+ '\t', ' '...'~', 0x80...0xFF => {},
+ else => return error.TypeError,
+ };
+ }
+
+ if (has_body and isNullBodyStatus(opts.status)) {
+ return error.TypeError;
+ }
+}
+
+// https://fetch.spec.whatwg.org/#null-body-status
+pub fn isNullBodyStatus(status: u16) bool {
+ return switch (status) {
+ 101, 103, 204, 205, 304 => true,
+ else => false,
+ };
+}
+
// fetch()'s response shell.
pub fn initPending(exec: *const Execution) !*Response {
const arena = try exec.session.getPinnedArena(.large, "Response.pending");
@@ -190,6 +218,7 @@ fn createRedirect(url_: []const u8, status_: ?u16, exec: *const Execution) !*Res
}
fn createJson(data: js.Value, opts_: ?InitOpts, exec: *const Execution) !*Response {
+ try validateInit(opts_ orelse .{}, true);
const session = exec.session;
const arena = try session.getPinnedArena(.medium, "Response.json");
errdefer arena.release();
diff --git a/src/testing.zig b/src/testing.zig
index 4443489b7..bea812062 100644
--- a/src/testing.zig
+++ b/src/testing.zig
@@ -866,6 +866,11 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void {
});
}
+ if (std.mem.startsWith(u8, path, "/status/")) {
+ const code = try std.fmt.parseInt(u16, path["/status/".len..], 10);
+ return req.respond("", .{ .status = @enumFromInt(code) });
+ }
+
if (std.mem.eql(u8, path, "/xhr/500")) {
return req.respond("Internal Server Error", .{
.status = .internal_server_error,