Skip to content

Commit 14266b4

Browse files
committed
enhance(node-fetch): use undici when available
1 parent 8cea12b commit 14266b4

21 files changed

+554
-239
lines changed

.changeset/serious-houses-flow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@whatwg-node/node-fetch': patch
3+
---
4+
5+
Use `undici` when available over `node:http`

jest.config.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ try {
1515
console.warn(`Failed to load uWebSockets.js. Skipping tests that require it.`, err);
1616
}
1717

18-
try {
19-
globals.libcurl = require('node-libcurl');
20-
} catch (err) {
21-
console.warn('Failed to load node-libcurl. Skipping tests that require it.', err);
18+
if (process.env.LEAK_TEST) {
19+
try {
20+
globals.TEST_LIBCURL = require('node-libcurl');
21+
} catch (err) {
22+
console.warn('Failed to load node-libcurl. Skipping tests that require it.', err);
23+
}
2224
}
2325

2426
module.exports = {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"prettier": "prettier --ignore-path .gitignore --ignore-path .prettierignore --write --list-different .",
2222
"prettier:check": "prettier --ignore-path .gitignore --ignore-path .prettierignore --check .",
2323
"release": "changeset publish",
24-
"test": "jest --runInBand --forceExit",
24+
"test": "jest --forceExit",
2525
"test:bun": "bun test --bail",
2626
"test:deno": "deno test ./packages/**/*.spec.ts --allow-all --fail-fast --no-check --unstable-sloppy-imports --trace-leaks",
2727
"test:leaks": "LEAK_TEST=1 jest --detectOpenHandles --detectLeaks --runInBand --forceExit",

packages/fetch/dist/node-ponyfill.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11

22
const createNodePonyfill = require('./create-node-ponyfill');
3-
const shouldSkipPonyfill = require('./shouldSkipPonyfill');
43
const ponyfills = createNodePonyfill();
54

6-
if (!shouldSkipPonyfill()) {
7-
try {
8-
const nodelibcurlName = 'node-libcurl'
9-
globalThis.libcurl = globalThis.libcurl || require(nodelibcurlName);
10-
} catch (e) { }
11-
}
12-
135
module.exports.fetch = ponyfills.fetch;
146
module.exports.Headers = ponyfills.Headers;
157
module.exports.Request = ponyfills.Request;

packages/node-fetch/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"devDependencies": {
4242
"@types/busboy": "1.5.4",
4343
"@types/pem": "^1.14.0",
44-
"pem": "^1.14.8"
44+
"pem": "^1.14.8",
45+
"undici": "^7.3.0"
4546
},
4647
"publishConfig": {
4748
"directory": "dist",

packages/node-fetch/src/Request.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ export class PonyfillRequest<TJSON = any> extends PonyfillBody<TJSON> implements
137137

138138
agent: HTTPAgent | HTTPSAgent | false | undefined;
139139

140+
dispatcher: import('undici').Dispatcher | undefined;
141+
140142
private _signal: AbortSignal | undefined | null;
141143

142144
get signal() {

packages/node-fetch/src/declarations.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ declare module '@kamilkisiela/fast-url-parser' {
3232
}
3333

3434
// TODO
35-
declare var libcurl: any;
35+
declare var TEST_LIBCURL: any;
3636
declare module 'scheduler/tracing' {
3737
export type Interaction = any;
3838
}

packages/node-fetch/src/fetch.ts

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Buffer } from 'node:buffer';
22
import { createReadStream } from 'node:fs';
33
import { fileURLToPath } from 'node:url';
4-
import { fetchCurl } from './fetchCurl.js';
4+
import { isPromise } from 'node:util/types';
5+
import { createFetchCurl } from './fetchCurl.js';
56
import { fetchNodeHttp } from './fetchNodeHttp.js';
7+
import { createFetchUndici } from './fetchUndici.js';
68
import { PonyfillRequest, RequestPonyfillInit } from './Request.js';
79
import { PonyfillResponse } from './Response.js';
810
import { PonyfillURL } from './URL.js';
@@ -57,15 +59,28 @@ function isURL(obj: any): obj is URL {
5759
return obj != null && obj.href != null;
5860
}
5961

60-
export function fetchPonyfill<TResponseJSON = any, TRequestJSON = any>(
61-
info: string | PonyfillRequest<TRequestJSON> | URL,
62-
init?: RequestPonyfillInit,
63-
): Promise<PonyfillResponse<TResponseJSON>> {
64-
if (typeof info === 'string' || isURL(info)) {
65-
const ponyfillRequest = new PonyfillRequest(info, init);
66-
return fetchPonyfill(ponyfillRequest);
67-
}
68-
const fetchRequest = info;
62+
let fetchFn$: Promise<typeof fetchNodeHttp>;
63+
let fetchFn: typeof fetchNodeHttp;
64+
65+
function getNativeGlobalDispatcher(): import('undici').Dispatcher {
66+
// @ts-expect-error - We know it is there
67+
return globalThis[Symbol.for('undici.globalDispatcher.1')];
68+
}
69+
70+
function createFetchFn() {
71+
const libcurlModuleName = 'node-libcurl';
72+
const undiciModuleName = 'undici';
73+
return import(libcurlModuleName).then(
74+
libcurl => createFetchCurl(libcurl),
75+
() =>
76+
import(undiciModuleName).then(
77+
(undici: typeof import('undici')) => createFetchUndici(() => undici.getGlobalDispatcher()),
78+
() => createFetchUndici(getNativeGlobalDispatcher),
79+
),
80+
);
81+
}
82+
83+
function fetchNonHttp(fetchRequest: PonyfillRequest) {
6984
if (fetchRequest.url.startsWith('data:')) {
7085
const response = getResponseForDataUri(fetchRequest.url);
7186
return fakePromise(response);
@@ -79,8 +94,54 @@ export function fetchPonyfill<TResponseJSON = any, TRequestJSON = any>(
7994
const response = getResponseForBlob(fetchRequest.url);
8095
return fakePromise(response);
8196
}
82-
if (globalThis.libcurl && !fetchRequest.agent) {
83-
return fetchCurl(fetchRequest);
97+
}
98+
99+
function normalizeInfo(info: string | PonyfillRequest | URL, init?: RequestPonyfillInit) {
100+
if (typeof info === 'string' || isURL(info)) {
101+
return new PonyfillRequest(info, init);
84102
}
85-
return fetchNodeHttp(fetchRequest);
103+
return info;
104+
}
105+
106+
export function createFetchPonyfill(fetchFn: typeof fetchNodeHttp) {
107+
return function fetchPonyfill<TResponseJSON = any, TRequestJSON = any>(
108+
info: string | PonyfillRequest<TRequestJSON> | URL,
109+
init?: RequestPonyfillInit,
110+
): Promise<PonyfillResponse<TResponseJSON>> {
111+
info = normalizeInfo(info, init);
112+
113+
const nonHttpRes = fetchNonHttp(info);
114+
115+
if (nonHttpRes) {
116+
return nonHttpRes;
117+
}
118+
119+
return fetchFn(info);
120+
};
121+
}
122+
123+
export function fetchPonyfill<TResponseJSON = any, TRequestJSON = any>(
124+
info: string | PonyfillRequest<TRequestJSON> | URL,
125+
init?: RequestPonyfillInit,
126+
): Promise<PonyfillResponse<TResponseJSON>> {
127+
info = normalizeInfo(info, init);
128+
129+
const nonHttpRes = fetchNonHttp(info);
130+
131+
if (nonHttpRes) {
132+
return nonHttpRes;
133+
}
134+
135+
if (!fetchFn) {
136+
fetchFn$ ||= createFetchFn();
137+
if (isPromise(fetchFn$)) {
138+
return fetchFn$.then(newFetchFn => {
139+
fetchFn = newFetchFn;
140+
return fetchFn(info);
141+
});
142+
}
143+
fetchFn = fetchFn$;
144+
}
145+
146+
return fetchFn(info);
86147
}

0 commit comments

Comments
 (0)