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,