diff --git a/src/browser/tests/net/fetch.html b/src/browser/tests/net/fetch.html
index 218051d27..fdd13dbb2 100644
--- a/src/browser/tests/net/fetch.html
+++ b/src/browser/tests/net/fetch.html
@@ -361,7 +361,7 @@
},
});
- const response = await fetch("http://127.0.0.1:9582/xhr/echo_post_body", {
+ const response = await fetch("http://127.0.0.1:9582/echo_body", {
method: "POST",
body: stream,
duplex: "half",
@@ -380,7 +380,7 @@
diff --git a/src/browser/webapi/net/body_init.zig b/src/browser/webapi/net/body_init.zig
index 9a32e380c..d47ecad31 100644
--- a/src/browser/webapi/net/body_init.zig
+++ b/src/browser/webapi/net/body_init.zig
@@ -106,8 +106,9 @@ pub const BodyInit = union(enum) {
},
.stream => |stream| {
// Response special-cases `.stream` before extract. Request/XHR
- // paths buffer a closed stream synchronously; open streams and
- // async-only bodies reject rather than send Content-Length: 0.
+ // paths buffer a closed stream synchronously; a stream that
+ // can't be drained here - still open, or already used as a
+ // body - rejects rather than send Content-Length: 0.
const bytes = try stream.collectBodyBytes(arena);
return .{ .bytes = bytes, .content_type = null };
},
diff --git a/src/browser/webapi/streams/ReadableStream.zig b/src/browser/webapi/streams/ReadableStream.zig
index 761074966..b1d63a2d0 100644
--- a/src/browser/webapi/streams/ReadableStream.zig
+++ b/src/browser/webapi/streams/ReadableStream.zig
@@ -53,6 +53,7 @@ _pull_fn: ?js.Function.Global = null,
_pulling: bool = false,
_pull_again: bool = false,
_cancel: ?Cancel = null,
+_collected: bool = false,
const UnderlyingSource = struct {
start: ?js.Function = null,
@@ -132,39 +133,44 @@ pub fn getLocked(self: *const ReadableStream) bool {
}
/// Drain a closed stream's internal queue into bytes for outbound HTTP
-/// request bodies. Open, errored, or locked streams, and chunks that cannot
-/// be converted synchronously, return `error.TypeError`.
+/// request bodies. A stream that is locked, already used as a body, still
+/// readable, or errored - or that holds a chunk which isn't string or binary
+/// data - is `error.TypeError`. The queue is only consumed once every chunk
+/// has converted, so a rejected body leaves the stream as it was.
pub fn collectBodyBytes(self: *ReadableStream, arena: std.mem.Allocator) ![]const u8 {
- if (self.getLocked()) return error.TypeError;
+ if (self._collected or self.getLocked()) {
+ return error.TypeError;
+ }
switch (self._state) {
.errored, .readable => return error.TypeError,
.closed => {},
}
- const exec = self._execution;
- const local = exec.js.local orelse return error.TypeError;
+ const local = self._execution.js.local.?;
var buf = std.Io.Writer.Allocating.init(arena);
- const controller = self._controller;
- while (controller.dequeue()) |chunk| {
+ const queue = &self._controller._queue;
+ for (queue.items) |chunk| {
const bytes: []const u8 = switch (chunk) {
.string => |s| s,
.uint8array => |arr| arr.values,
.js_value => |global| blk: {
const value = local.toLocal(global);
- if (value.isTypedArray() or value.isArrayBufferView() or value.isArrayBuffer()) {
- const typed = try local.jsValueToZig([]u8, value);
- break :blk try arena.dupe(u8, typed);
- } else if (value.isString()) |str| {
- const slice = try str.toSlice();
- break :blk try arena.dupe(u8, slice);
- } else {
+ // toStringSmart falls back to toString(), which would turn a
+ // stray number or object into "42" / "[object Object]" bytes.
+ if (value.isString() == null and !value.isTypedArray() and
+ !value.isArrayBufferView() and !value.isArrayBuffer())
+ {
return error.TypeError;
}
+ break :blk try value.toStringSmart();
},
};
try buf.writer.writeAll(bytes);
}
+
+ self._collected = true;
+ queue.clearRetainingCapacity();
return buf.written();
}
diff --git a/src/testing.zig b/src/testing.zig
index 4daac138a..05ff60607 100644
--- a/src/testing.zig
+++ b/src/testing.zig
@@ -970,10 +970,12 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void {
});
}
- if (std.mem.eql(u8, path, "/xhr/echo_post_body")) {
+ if (std.mem.eql(u8, path, "/echo_body")) {
+ // Echo the request body back verbatim, so tests can assert on the bytes
+ // a request actually sent rather than just on its status.
var body_buf: [4096]u8 = undefined;
const body = if (req.head.method.requestHasBody())
- req.readerExpectNone(&body_buf).allocRemaining(arena_allocator, .limited(body_buf.len)) catch ""
+ try req.readerExpectNone(&body_buf).allocRemaining(arena_allocator, .limited(body_buf.len))
else
"";
return req.respond(body, .{