Skip to content

Commit 0d9cad2

Browse files
committed
fix: clone methods correctly handle teeing body stream
1 parent ca8b7df commit 0d9cad2

File tree

5 files changed

+75
-16
lines changed

5 files changed

+75
-16
lines changed

package-lock.json

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Body.js

+15
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class Body {
66
this._bodyInit = body;
77

88
if (!body) {
9+
this._bodyNull = true;
910
this._bodyText = "";
1011
return this;
1112
}
@@ -179,6 +180,10 @@ class Body {
179180
}
180181

181182
get body() {
183+
if (this._bodyNull) {
184+
return null;
185+
}
186+
182187
if (this._bodyReadableStream) {
183188
return this._bodyReadableStream;
184189
}
@@ -228,6 +233,16 @@ class Body {
228233
},
229234
});
230235
}
236+
237+
clone() {
238+
if (this._bodyReadableStream) {
239+
const [stream1, stream2] = this._bodyReadableStream.tee();
240+
this._bodyReadableStream = stream1;
241+
return new Body(stream2);
242+
} else {
243+
return new Body(this._bodyInit);
244+
}
245+
}
231246
}
232247

233248
export default Body;

src/Request.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class Request {
4646
this.signal = request.signal;
4747
this.headers = new Headers(options.headers ?? request.headers);
4848

49-
if (!options.body && request._body._bodyInit) {
49+
if (options.body === undefined && request._body._bodyInit) {
5050
this._body = new Body(request._body._bodyInit);
5151
request._body.bodyUsed = true;
5252
}
@@ -85,7 +85,14 @@ class Request {
8585
}
8686

8787
clone() {
88-
return new Request(this, { body: this._body._bodyInit });
88+
if (this.bodyUsed) {
89+
throw new TypeError("Already read");
90+
}
91+
92+
const newRequest = new Request(this, { body: null });
93+
newRequest._body = this._body.clone();
94+
95+
return newRequest;
8996
}
9097

9198
blob() {

src/Response.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,19 @@ class Response {
2121
}
2222

2323
clone() {
24-
return new Response(this._body._bodyInit, {
24+
if (this.bodyUsed) {
25+
throw new TypeError("Already read");
26+
}
27+
28+
const newResponse = new Response(null, {
2529
status: this.status,
2630
statusText: this.statusText,
2731
headers: new Headers(this.headers),
2832
url: this.url,
2933
});
34+
newResponse._body = this._body.clone();
35+
36+
return newResponse;
3037
}
3138

3239
blob() {

test/index.js

+41-12
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ test("request", (t) => {
553553
t.isNot(clone.headers, req.headers);
554554
t.notOk(req.bodyUsed);
555555

556-
const bodies = await Promise.all([clone.text(), req.clone().text()]);
556+
const bodies = await Promise.all([clone.text(), req.text()]);
557557

558558
t.eq(bodies, ["I work out", "I work out"]);
559559
});
@@ -1864,14 +1864,27 @@ test("fetch method", (t) => {
18641864
reactNative: { textStreaming: true },
18651865
});
18661866
const clone = res.clone();
1867-
const stream = await clone.body;
1868-
const text = new TextDecoder().decode(await drainStream(stream));
1867+
1868+
const resStream = await res.body;
1869+
const cloneStream = await clone.body;
1870+
const resText = new TextDecoder().decode(
1871+
await drainStream(resStream)
1872+
);
1873+
const cloneText = new TextDecoder().decode(
1874+
await drainStream(cloneStream)
1875+
);
18691876

18701877
t.ok(
1871-
stream instanceof ReadableStream,
1878+
resStream instanceof ReadableStream,
18721879
"Response implements streaming body"
18731880
);
1874-
t.eq(text, "Hello world!");
1881+
t.eq(resText, "Hello world!");
1882+
1883+
t.ok(
1884+
cloneStream instanceof ReadableStream,
1885+
"Response implements streaming body"
1886+
);
1887+
t.eq(cloneText, "Hello world!");
18751888
});
18761889

18771890
t.test("cloning blob response", async (t) => {
@@ -1882,8 +1895,11 @@ test("fetch method", (t) => {
18821895
},
18831896
});
18841897
const clone = res.clone();
1885-
const json = await clone.json();
1886-
t.eq(json.headers.accept, "application/json");
1898+
const resJson = await res.json();
1899+
const cloneJson = await clone.json();
1900+
1901+
t.eq(resJson.headers.accept, "application/json");
1902+
t.eq(cloneJson.headers.accept, "application/json");
18871903
});
18881904

18891905
t.test("cloning array buffer response", async (t) => {
@@ -1894,15 +1910,28 @@ test("fetch method", (t) => {
18941910
},
18951911
});
18961912
const clone = res.clone();
1897-
const buf = await clone.arrayBuffer();
1913+
const resBuf = await res.arrayBuffer();
1914+
const cloneBuf = await clone.arrayBuffer();
18981915

1899-
t.ok(buf instanceof ArrayBuffer, "buf is an ArrayBuffer instance");
1900-
t.eq(buf.byteLength, 256, "buf.byteLength is correct");
1916+
t.ok(
1917+
resBuf instanceof ArrayBuffer,
1918+
"buf is an ArrayBuffer instance"
1919+
);
1920+
t.eq(resBuf.byteLength, 256, "buf.byteLength is correct");
1921+
1922+
t.ok(
1923+
cloneBuf instanceof ArrayBuffer,
1924+
"buf is an ArrayBuffer instance"
1925+
);
1926+
t.eq(cloneBuf.byteLength, 256, "buf.byteLength is correct");
19011927

19021928
const expected = Array.from({ length: 256 }, (_, i) => i);
1903-
const actual = Array.from(new Uint8Array(buf));
19041929

1905-
t.eq(actual, expected);
1930+
const resActual = Array.from(new Uint8Array(resBuf));
1931+
const cloneActual = Array.from(new Uint8Array(cloneBuf));
1932+
1933+
t.eq(resActual, expected);
1934+
t.eq(cloneActual, expected);
19061935
});
19071936
});
19081937
});

0 commit comments

Comments
 (0)