fix(net): convert js_value stream chunks for fetch POST bodies

JS controller.enqueue stores chunks as .js_value via enqueueValue, so
collectBodyBytes must extract TypedArray/ArrayBuffer/string bytes with
local scope and arena.dupe before buffering. Add multi-chunk success and
open/locked reject fixtures in fetch.html.
This commit is contained in:
arimu1
2026-07-25 09:19:23 +07:00
parent f2ddaee8d1
commit fdc98b7e4b
2 changed files with 62 additions and 3 deletions

View File

@@ -352,9 +352,11 @@
{
const state = await testing.async();
const payload = "name=Streamed&count=9";
const enc = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(payload));
controller.enqueue(enc.encode("name=Streamed&"));
controller.enqueue(enc.encode("count=9"));
controller.close();
},
});
@@ -375,6 +377,44 @@
}
</script>
<script id=fetch_readablestream_body_reject type=module>
{
const state = await testing.async();
const url = "http://127.0.0.1:9582/xhr/echo_post_body";
const opts = { method: "POST", duplex: "half" };
let openRejected = false;
try {
const open = new ReadableStream({
start(c) { c.enqueue(new TextEncoder().encode("x")); },
});
await fetch(url, { ...opts, body: open });
} catch (e) {
openRejected = e instanceof TypeError;
}
let lockedRejected = false;
try {
const locked = new ReadableStream({
start(c) {
c.enqueue(new TextEncoder().encode("x"));
c.close();
},
});
locked.getReader();
await fetch(url, { ...opts, body: locked });
} catch (e) {
lockedRejected = e instanceof TypeError;
}
state.resolve();
await state.done(() => {
testing.expectEqual(true, openRejected);
testing.expectEqual(true, lockedRejected);
});
}
</script>
<script id=fetch_blob_url_in_worker type=module>
{
const state = await testing.async();

View File

@@ -141,13 +141,32 @@ pub fn collectBodyBytes(self: *ReadableStream, arena: std.mem.Allocator) ![]cons
.closed => {},
}
const exec = self._execution;
if (exec.js.local == null) return error.TypeError;
var ls: js.Local.Scope = undefined;
exec.js.localScope(&ls);
defer ls.deinit();
const local = &ls;
var buf = std.Io.Writer.Allocating.init(arena);
const controller = self._controller;
while (controller.dequeue()) |chunk| {
const bytes = switch (chunk) {
const bytes: []const u8 = switch (chunk) {
.string => |s| s,
.uint8array => |arr| arr.values,
.js_value => return error.TypeError,
.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 {
return error.TypeError;
}
},
};
try buf.writer.writeAll(bytes);
}