Skip to content

Navigation Menu

Sign in
Sign up

Commit a846852

Browse files
fix(node-fetch): use stream.Readable instead of web streams (oven-sh#4394)
* fix blobFrom * fix(node-fetch): use stream.Readable instead of web streams * uncomment * comment why
1 parent 3f4bc62 commit a846852

10 files changed

Lines changed: 209 additions & 106 deletions

File tree

‎src/js/_codegen/build-modules.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ globalThis.requireTransformer = (specifier: string, from: string) => {
8484
const found = moduleList.indexOf(path.relative(BASE, relativeMatch));
8585
if (found === -1) {
8686
throw new Error(
87-
`Builtin Bundler: "${specifier}" cannot be imported here because it doesn't get a module ID. Only files in "src/js" besides "src/js/builtins" can be used here.`,
87+
`Builtin Bundler: "${specifier}" cannot be imported here because it doesn't get a module ID. Only files in "src/js" besides "src/js/builtins" can be used here. Note that the 'node:' or 'bun:' prefix is required here. `,
8888
);
8989
}
9090
return codegenRequireId(`${found}/*${path.relative(BASE, relativeMatch)}*/`);

‎src/js/node/stream.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3327,7 +3327,7 @@ var require_readable = __commonJS({
33273327

33283328
streamReadable.pause();
33293329

3330-
const cleanup = finished(streamReadable, error => {
3330+
const cleanup = eos(streamReadable, error => {
33313331
if (error?.code === "ERR_STREAM_PREMATURE_CLOSE") {
33323332
const err = new AbortError(undefined, { cause: error });
33333333
error = err;

‎src/js/out/InternalModuleRegistryConstants.h‎

Lines changed: 6 additions & 6 deletions
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const bunFetch = Bun.fetch;
2-
const fetch = (...args) => bunFetch(...args);
2+
const fetch = (...args: Parameters<typeofbunFetch>) => bunFetch(...args);
33
fetch.default = fetch;
44
fetch.fetch = fetch;
55
export default fetch;

‎src/js/thirdparty/node-fetch.js‎

Lines changed: 0 additions & 64 deletions
This file was deleted.

‎src/js/thirdparty/node-fetch.ts‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type * as s from "stream";
2+
3+
const { Headers, Request, Response: WebResponse, Blob, File = Blob, FormData } = globalThis as any;
4+
const nativeFetch = Bun.fetch;
5+
6+
const { Readable } = require("node:stream");
7+
8+
class Response extends WebResponse {
9+
_body: any;
10+
11+
get body() {
12+
return this._body ?? (this._body = Readable.fromWeb(super.body));
13+
}
14+
}
15+
16+
/**
17+
* `node-fetch` works like the browser-fetch API, except it's a little more strict on some features,
18+
* and uses node streams instead of web streams.
19+
*
20+
* It's overall a positive on speed to override the implementation, since most people will use something
21+
* like `.json()` or `.text()`, which is faster in Bun's native fetch, vs `node-fetch` going
22+
* through `node:http`, a node stream, then processing the data.
23+
*/
24+
async function fetch(url: any, init?: RequestInit & { body?: any }) {
25+
// input node stream -> web stream
26+
let body: s.Readable | undefined = init?.body;
27+
if (body) {
28+
const chunks: any = [];
29+
if (body instanceof Readable) {
30+
// TODO: Bun fetch() doesn't support ReadableStream at all.
31+
for await (const chunk of body) {
32+
chunks.push(chunk);
33+
}
34+
init = { ...init, body: new Blob(chunks) };
35+
}
36+
}
37+
38+
const response = await nativeFetch(url, init);
39+
Object.setPrototypeOf(response, Response.prototype);
40+
return response;
41+
}
42+
43+
class AbortError extends DOMException {
44+
constructor(message) {
45+
super(message, "AbortError");
46+
}
47+
}
48+
49+
class FetchBaseError extends Error {
50+
type: string;
51+
52+
constructor(message, type) {
53+
super(message);
54+
this.type = type;
55+
}
56+
}
57+
58+
class FetchError extends FetchBaseError {
59+
constructor(message, type, systemError) {
60+
super(message, type);
61+
this.code = systemError?.code;
62+
}
63+
}
64+
65+
function blobFrom(path, options) {
66+
return Promise.resolve(Bun.file(path, options));
67+
}
68+
69+
function blobFromSync(path, options) {
70+
return Bun.file(path, options);
71+
}
72+
73+
var fileFrom = blobFrom;
74+
var fileFromSync = blobFromSync;
75+
76+
function isRedirect(code) {
77+
return code === 301 || code === 302 || code === 303 || code === 307 || code === 308;
78+
}
79+
80+
export default Object.assign(fetch, {
81+
AbortError,
82+
Blob,
83+
FetchBaseError,
84+
FetchError,
85+
File,
86+
FormData,
87+
Headers,
88+
Request,
89+
Response,
90+
blobFrom,
91+
blobFromSync,
92+
fileFrom,
93+
fileFromSync,
94+
isRedirect,
95+
fetch,
96+
default: fetch,
97+
});

‎test/js/node/fs/node-fetch.test.js‎

Lines changed: 0 additions & 33 deletions
This file was deleted.

‎test/js/node/http/node-fetch.test.js‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import fetch2, { fetch, Response, Request, Headers } from "node-fetch";
2+
import * as iso from "isomorphic-fetch";
3+
import * as vercelFetch from "@vercel/fetch";
4+
import * as stream from "stream";
5+
6+
import { test, expect } from "bun:test";
7+
8+
test("node-fetch", () => {
9+
expect(Response.prototype).toBeInstanceOf(globalThis.Response);
10+
expect(Request).toBe(globalThis.Request);
11+
expect(Headers).toBe(globalThis.Headers);
12+
expect(fetch2.default).toBe(fetch2);
13+
expect(fetch2.Response).toBe(Response);
14+
});
15+
16+
for (const [impl, name] of [
17+
[fetch, "node-fetch.fetch"],
18+
[fetch2, "node-fetch.default"],
19+
[fetch2.default, "node-fetch.default.default"],
20+
[iso.fetch, "isomorphic-fetch.fetch"],
21+
[iso.default.fetch, "isomorphic-fetch.default.fetch"],
22+
[iso.default, "isomorphic-fetch.default"],
23+
[vercelFetch.default(fetch), "@vercel/fetch.default"],
24+
]) {
25+
test(name + " fetches", async () => {
26+
const server = Bun.serve({
27+
port: 0,
28+
fetch(req, server) {
29+
server.stop();
30+
return new Response("it works");
31+
},
32+
});
33+
expect(await impl("http://" + server.hostname + ":" + server.port)).toBeInstanceOf(globalThis.Response);
34+
server.stop(true);
35+
});
36+
}
37+
38+
test("node-fetch uses node streams instead of web streams", async () => {
39+
const server = Bun.serve({
40+
port: 0,
41+
async fetch(req, server) {
42+
const body = await req.text();
43+
expect(body).toBe("the input text");
44+
return new Response("hello world");
45+
},
46+
});
47+
48+
try {
49+
const result = await fetch2("http://" + server.hostname + ":" + server.port, {
50+
body: new stream.Readable({
51+
read() {
52+
this.push("the input text");
53+
this.push(null);
54+
},
55+
}),
56+
method: "POST",
57+
});
58+
expect(result.body).toBeInstanceOf(stream.Readable);
59+
expect(result.body === result.body).toBe(true); // cached lazy getter
60+
const chunks = [];
61+
for await (const chunk of result.body) {
62+
chunks.push(chunk);
63+
}
64+
expect(Buffer.concat(chunks).toString()).toBe("hello world");
65+
} finally {
66+
server.stop(true);
67+
}
68+
});

‎test/js/node/stream/node-stream.test.js‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,3 +314,38 @@ it("TTY streams", () => {
314314
expect(stderr.toString()).toContain("0 fail");
315315
expect(exitCode).toBe(0);
316316
});
317+
318+
it("Readable.toWeb", async () => {
319+
const readable = new Readable({
320+
read() {
321+
this.push("Hello ");
322+
this.push("World!\n");
323+
this.push(null);
324+
},
325+
});
326+
327+
const webReadable = Readable.toWeb(readable);
328+
expect(webReadable).toBeInstanceOf(ReadableStream);
329+
330+
const result = await new Response(webReadable).text();
331+
expect(result).toBe("Hello World!\n");
332+
});
333+
334+
it("Readable.fromWeb", async () => {
335+
const readable = Readable.fromWeb(
336+
new ReadableStream({
337+
start(controller) {
338+
controller.enqueue("Hello ");
339+
controller.enqueue("World!\n");
340+
controller.close();
341+
},
342+
}),
343+
);
344+
expect(readable).toBeInstanceOf(Readable);
345+
346+
const chunks = [];
347+
for await (const chunk of readable) {
348+
chunks.push(chunk);
349+
}
350+
expect(Buffer.concat(chunks).toString()).toBe("Hello World!\n");
351+
});

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /