Skip to content

Commit

Permalink
enhance(node-fetch): use undici when available
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Jan 22, 2025
1 parent 4fff5c5 commit 8725fae
Show file tree
Hide file tree
Showing 21 changed files with 554 additions and 239 deletions.
5 changes: 5 additions & 0 deletions .changeset/serious-houses-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@whatwg-node/node-fetch': patch
---

Use `undici` when available over `node:http`
10 changes: 6 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ try {
console.warn(`Failed to load uWebSockets.js. Skipping tests that require it.`, err);
}

try {
globals.libcurl = require('node-libcurl');
} catch (err) {
console.warn('Failed to load node-libcurl. Skipping tests that require it.', err);
if (process.env.LEAK_TEST) {
try {
globals.TEST_LIBCURL = require('node-libcurl');
} catch (err) {
console.warn('Failed to load node-libcurl. Skipping tests that require it.', err);
}
}

module.exports = {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"prettier": "prettier --ignore-path .gitignore --ignore-path .prettierignore --write --list-different .",
"prettier:check": "prettier --ignore-path .gitignore --ignore-path .prettierignore --check .",
"release": "changeset publish",
"test": "jest --runInBand --forceExit",
"test": "jest --forceExit",
"test:bun": "bun test --bail",
"test:deno": "deno test ./packages/**/*.spec.ts --allow-all --fail-fast --no-check --unstable-sloppy-imports --trace-leaks",
"test:leaks": "LEAK_TEST=1 jest --detectOpenHandles --detectLeaks --runInBand --forceExit",
Expand Down
8 changes: 0 additions & 8 deletions packages/fetch/dist/node-ponyfill.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@

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

if (!shouldSkipPonyfill()) {
try {
const nodelibcurlName = 'node-libcurl'
globalThis.libcurl = globalThis.libcurl || require(nodelibcurlName);
} catch (e) { }
}

module.exports.fetch = ponyfills.fetch;
module.exports.Headers = ponyfills.Headers;
module.exports.Request = ponyfills.Request;
Expand Down
3 changes: 2 additions & 1 deletion packages/node-fetch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"devDependencies": {
"@types/busboy": "1.5.4",
"@types/pem": "^1.14.0",
"pem": "^1.14.8"
"pem": "^1.14.8",
"undici": "^7.3.0"
},
"publishConfig": {
"directory": "dist",
Expand Down
2 changes: 2 additions & 0 deletions packages/node-fetch/src/Request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ export class PonyfillRequest<TJSON = any> extends PonyfillBody<TJSON> implements

agent: HTTPAgent | HTTPSAgent | false | undefined;

dispatcher: import('undici').Dispatcher | undefined;

private _signal: AbortSignal | undefined | null;

get signal() {
Expand Down
2 changes: 1 addition & 1 deletion packages/node-fetch/src/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ declare module '@kamilkisiela/fast-url-parser' {
}

// TODO
declare var libcurl: any;
declare var TEST_LIBCURL: any;
declare module 'scheduler/tracing' {
export type Interaction = any;
}
87 changes: 74 additions & 13 deletions packages/node-fetch/src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Buffer } from 'node:buffer';
import { createReadStream } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { fetchCurl } from './fetchCurl.js';
import { isPromise } from 'node:util/types';
import { createFetchCurl } from './fetchCurl.js';
import { fetchNodeHttp } from './fetchNodeHttp.js';
import { createFetchUndici } from './fetchUndici.js';
import { PonyfillRequest, RequestPonyfillInit } from './Request.js';
import { PonyfillResponse } from './Response.js';
import { PonyfillURL } from './URL.js';
Expand Down Expand Up @@ -57,15 +59,28 @@ function isURL(obj: any): obj is URL {
return obj != null && obj.href != null;
}

export function fetchPonyfill<TResponseJSON = any, TRequestJSON = any>(
info: string | PonyfillRequest<TRequestJSON> | URL,
init?: RequestPonyfillInit,
): Promise<PonyfillResponse<TResponseJSON>> {
if (typeof info === 'string' || isURL(info)) {
const ponyfillRequest = new PonyfillRequest(info, init);
return fetchPonyfill(ponyfillRequest);
}
const fetchRequest = info;
let fetchFn$: Promise<typeof fetchNodeHttp>;
let fetchFn: typeof fetchNodeHttp;

function getNativeGlobalDispatcher(): import('undici').Dispatcher {
// @ts-expect-error - We know it is there
return globalThis[Symbol.for('undici.globalDispatcher.1')];
}

function createFetchFn() {
const libcurlModuleName = 'node-libcurl';
const undiciModuleName = 'undici';
return import(libcurlModuleName).then(
libcurl => createFetchCurl(libcurl),
() =>
import(undiciModuleName).then(
(undici: typeof import('undici')) => createFetchUndici(() => undici.getGlobalDispatcher()),
() => createFetchUndici(getNativeGlobalDispatcher),
),
);
}

function fetchNonHttp(fetchRequest: PonyfillRequest) {
if (fetchRequest.url.startsWith('data:')) {
const response = getResponseForDataUri(fetchRequest.url);
return fakePromise(response);
Expand All @@ -79,8 +94,54 @@ export function fetchPonyfill<TResponseJSON = any, TRequestJSON = any>(
const response = getResponseForBlob(fetchRequest.url);
return fakePromise(response);
}
if (globalThis.libcurl && !fetchRequest.agent) {
return fetchCurl(fetchRequest);
}

function normalizeInfo(info: string | PonyfillRequest | URL, init?: RequestPonyfillInit) {
if (typeof info === 'string' || isURL(info)) {
return new PonyfillRequest(info, init);
}
return fetchNodeHttp(fetchRequest);
return info;
}

export function createFetchPonyfill(fetchFn: typeof fetchNodeHttp) {
return function fetchPonyfill<TResponseJSON = any, TRequestJSON = any>(
info: string | PonyfillRequest<TRequestJSON> | URL,
init?: RequestPonyfillInit,
): Promise<PonyfillResponse<TResponseJSON>> {
info = normalizeInfo(info, init);

const nonHttpRes = fetchNonHttp(info);

if (nonHttpRes) {
return nonHttpRes;
}

return fetchFn(info);
};
}

export function fetchPonyfill<TResponseJSON = any, TRequestJSON = any>(
info: string | PonyfillRequest<TRequestJSON> | URL,
init?: RequestPonyfillInit,
): Promise<PonyfillResponse<TResponseJSON>> {
info = normalizeInfo(info, init);

const nonHttpRes = fetchNonHttp(info);

if (nonHttpRes) {
return nonHttpRes;
}

if (!fetchFn) {
fetchFn$ ||= createFetchFn();
if (isPromise(fetchFn$)) {
return fetchFn$.then(newFetchFn => {
fetchFn = newFetchFn;
return fetchFn(info);
});
}
fetchFn = fetchFn$;
}

return fetchFn(info);
}
Loading

0 comments on commit 8725fae

Please sign in to comment.