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

Commit 015e1d6

Browse files
authored
Fix cloning of Responses constructed with byte streams, closes #527 (#543)
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 eaed542 commit 015e1d6

File tree

2 files changed

+47
-19
lines changed

2 files changed

+47
-19
lines changed

packages/core/src/standards/http.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,13 @@ export class Response<
570570
[kWaitUntil]?: Promise<WaitUntil>;
571571

572572
constructor(body?: BodyInit, init?: ResponseInit | Response | BaseResponse) {
573+
// If body is a FixedLengthStream, set Content-Length to its expected
574+
// length. We may replace `body` later on with a different stream, so
575+
// extract `contentLength` now.
576+
const contentLength: number | undefined = (
577+
body as { [kContentLength]?: number }
578+
)?.[kContentLength];
579+
573580
let encodeBody: string | undefined;
574581
let status: number | undefined;
575582
let webSocket: WebSocket | undefined;
@@ -582,6 +589,15 @@ export class Response<
582589
if (body instanceof ArrayBuffer) {
583590
body = body.slice(0);
584591
}
592+
if (body instanceof ReadableStream && _isByteStream(body)) {
593+
// Since `[email protected]`, cloning a body now invokes `structuredClone`
594+
// on the underlying stream (https://github.com/nodejs/undici/pull/1697).
595+
// Unfortunately, due to a bug in Node, byte streams cannot be
596+
// `structuredClone`d (https://github.com/nodejs/undici/issues/1873,
597+
// https://github.com/nodejs/node/pull/45955), leading to issues when
598+
// constructing bodies with byte streams.
599+
body = convertToRegularStream(body);
600+
}
585601

586602
if (init instanceof Response) {
587603
encodeBody = init.#encodeBody;
@@ -628,10 +644,6 @@ export class Response<
628644
this.#status = status;
629645
this.#webSocket = webSocket;
630646

631-
// If body is a FixedLengthStream, set Content-Length to its expected length
632-
const contentLength: number | undefined = (
633-
body as { [kContentLength]?: number }
634-
)?.[kContentLength];
635647
if (contentLength !== undefined) {
636648
this.headers.set("content-length", contentLength.toString());
637649
}

packages/core/test/standards/http.spec.ts

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -129,21 +129,6 @@ test("Body: same body instance is always returned", (t) => {
129129
t.not(body.body, null);
130130
t.is(body.body, body.body);
131131
});
132-
test("Body: reuses byte stream if not input gated", (t) => {
133-
const bodyStream = new ReadableStream({
134-
type: "bytes",
135-
pull(controller) {
136-
controller.enqueue(utf8Encode("chunk"));
137-
controller.close();
138-
},
139-
});
140-
141-
let res = new Response(bodyStream);
142-
t.is(res.body, bodyStream);
143-
144-
res = withInputGating(new Response(bodyStream));
145-
t.not(res.body, bodyStream);
146-
});
147132
test("Body: body isn't locked until read from", async (t) => {
148133
const res = new Response("body");
149134
// noinspection SuspiciousTypeOfGuard
@@ -869,6 +854,37 @@ test("Response: clones non-standard properties", async (t) => {
869854
t.is(await res2.text(), "body");
870855
t.is(await res3.text(), "body");
871856
});
857+
test("Response: clones stream bodies", async (t) => {
858+
let stream = new ReadableStream({
859+
start(controller) {
860+
controller.enqueue(utf8Encode("chunk1"));
861+
controller.close();
862+
},
863+
});
864+
let res = new Response(stream);
865+
let clone = res.clone();
866+
assert(res.body !== null && clone.body !== null);
867+
t.true(_isByteStream(res.body));
868+
t.true(_isByteStream(clone.body));
869+
t.is(await res.text(), "chunk1");
870+
t.is(await clone.text(), "chunk1");
871+
872+
// Check again with byte stream
873+
stream = new ReadableStream({
874+
type: "bytes",
875+
start(controller) {
876+
controller.enqueue(utf8Encode("chunk2"));
877+
controller.close();
878+
},
879+
});
880+
res = new Response(stream);
881+
clone = res.clone();
882+
assert(res.body !== null && clone.body !== null);
883+
t.true(_isByteStream(res.body));
884+
t.true(_isByteStream(clone.body));
885+
t.is(await res.text(), "chunk2");
886+
t.is(await clone.text(), "chunk2");
887+
});
872888
test("Response: constructing from Response copies non-standard properties", (t) => {
873889
const pair = new WebSocketPair();
874890
const res = new Response("body1", {

0 commit comments

Comments
 (0)