Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 0a2785d

Browse files
committed
Fix cloning of Responses constructed with byte streams, closes #527
This applies the same fix for `Request` bodies from #510 to `Response`s. Notably, byte streams are returned from lots of Workers runtime APIs (e.g. KV, R2) to support BYOB reads. It's likely these are then used in `Response`s and cloned for caching.
1 parent a654865 commit 0a2785d

File tree

2 files changed

+40
-15
lines changed

2 files changed

+40
-15
lines changed

Diff for: packages/core/src/standards/http.ts

+9
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,15 @@ export class Response<
582582
if (body instanceof ArrayBuffer) {
583583
body = body.slice(0);
584584
}
585+
if (body instanceof ReadableStream && _isByteStream(body)) {
586+
// Since `[email protected]`, cloning a body now invokes `structuredClone`
587+
// on the underlying stream (https://github.com/nodejs/undici/pull/1697).
588+
// Unfortunately, due to a bug in Node, byte streams cannot be
589+
// `structuredClone`d (https://github.com/nodejs/undici/issues/1873,
590+
// https://github.com/nodejs/node/pull/45955), leading to issues when
591+
// constructing bodies with byte streams.
592+
body = convertToRegularStream(body);
593+
}
585594

586595
if (init instanceof Response) {
587596
encodeBody = init.#encodeBody;

Diff for: packages/core/test/standards/http.spec.ts

+31-15
Original file line numberDiff line numberDiff line change
@@ -128,21 +128,6 @@ test("Body: same body instance is always returned", (t) => {
128128
t.not(body.body, null);
129129
t.is(body.body, body.body);
130130
});
131-
test("Body: reuses byte stream if not input gated", (t) => {
132-
const bodyStream = new ReadableStream({
133-
type: "bytes",
134-
pull(controller) {
135-
controller.enqueue(utf8Encode("chunk"));
136-
controller.close();
137-
},
138-
});
139-
140-
let res = new Response(bodyStream);
141-
t.is(res.body, bodyStream);
142-
143-
res = withInputGating(new Response(bodyStream));
144-
t.not(res.body, bodyStream);
145-
});
146131
test("Body: body isn't locked until read from", async (t) => {
147132
const res = new Response("body");
148133
// noinspection SuspiciousTypeOfGuard
@@ -845,6 +830,37 @@ test("Response: clones non-standard properties", async (t) => {
845830
t.is(await res2.text(), "body");
846831
t.is(await res3.text(), "body");
847832
});
833+
test("Response: clones stream bodies", async (t) => {
834+
let stream = new ReadableStream({
835+
start(controller) {
836+
controller.enqueue(utf8Encode("chunk1"));
837+
controller.close();
838+
},
839+
});
840+
let res = new Response(stream);
841+
let clone = res.clone();
842+
assert(res.body !== null && clone.body !== null);
843+
t.true(_isByteStream(res.body));
844+
t.true(_isByteStream(clone.body));
845+
t.is(await res.text(), "chunk1");
846+
t.is(await clone.text(), "chunk1");
847+
848+
// Check again with byte stream
849+
stream = new ReadableStream({
850+
type: "bytes",
851+
start(controller) {
852+
controller.enqueue(utf8Encode("chunk2"));
853+
controller.close();
854+
},
855+
});
856+
res = new Response(stream);
857+
clone = res.clone();
858+
assert(res.body !== null && clone.body !== null);
859+
t.true(_isByteStream(res.body));
860+
t.true(_isByteStream(clone.body));
861+
t.is(await res.text(), "chunk2");
862+
t.is(await clone.text(), "chunk2");
863+
});
848864
test("Response: constructing from Response copies non-standard properties", (t) => {
849865
const pair = new WebSocketPair();
850866
const res = new Response("body1", {

0 commit comments

Comments
 (0)