@pnpm/network.fetch 1000.2.6 → 1100.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ import type { TlsConfig } from '@pnpm/types';
2
+ import { type Dispatcher } from 'undici';
3
+ export type ClientCertificates = Record<string, TlsConfig>;
4
+ export interface DispatcherOptions {
5
+ ca?: string | string[] | Buffer;
6
+ cert?: string | string[] | Buffer;
7
+ key?: string | Buffer;
8
+ localAddress?: string;
9
+ maxSockets?: number;
10
+ strictSsl?: boolean;
11
+ timeout?: number;
12
+ httpProxy?: string;
13
+ httpsProxy?: string;
14
+ noProxy?: boolean | string;
15
+ clientCertificates?: ClientCertificates;
16
+ }
17
+ /**
18
+ * Clear the dispatcher cache. Useful for testing.
19
+ */
20
+ export declare function clearDispatcherCache(): void;
21
+ /**
22
+ * Get a dispatcher for the given URI and options.
23
+ * Returns undefined if no special configuration is needed (to use global dispatcher).
24
+ */
25
+ export declare function getDispatcher(uri: string, opts: DispatcherOptions): Dispatcher | undefined;
@@ -0,0 +1,323 @@
1
+ import net from 'node:net';
2
+ import tls from 'node:tls';
3
+ import { URL } from 'node:url';
4
+ import { nerfDart } from '@pnpm/config.nerf-dart';
5
+ import { PnpmError } from '@pnpm/error';
6
+ import { LRUCache } from 'lru-cache';
7
+ import { SocksClient } from 'socks';
8
+ import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici';
9
+ const DEFAULT_MAX_SOCKETS = 50;
10
+ const KEEP_ALIVE_TIMEOUT = 30_000; // 30 seconds
11
+ const KEEP_ALIVE_MAX_TIMEOUT = 600_000; // 10 minutes
12
+ // Set an optimized global dispatcher so that requests without custom options
13
+ // (no proxy, no custom certs) still benefit from better keep-alive and Happy Eyeballs.
14
+ //
15
+ // Note: we intentionally do NOT enable HTTP/2 (allowH2) or HTTP/1.1 pipelining here.
16
+ // With HTTP/2, undici multiplexes many streams over 1-2 TCP connections sharing a single
17
+ // congestion window. In benchmarks this was slower than opening ~50 independent HTTP/1.1
18
+ // connections that each get their own congestion window and can saturate bandwidth in parallel.
19
+ setGlobalDispatcher(new Agent({
20
+ connections: DEFAULT_MAX_SOCKETS,
21
+ keepAliveTimeout: KEEP_ALIVE_TIMEOUT,
22
+ keepAliveMaxTimeout: KEEP_ALIVE_MAX_TIMEOUT,
23
+ connect: {
24
+ autoSelectFamily: true,
25
+ },
26
+ }));
27
+ const DISPATCHER_CACHE = new LRUCache({
28
+ max: 50,
29
+ dispose: (dispatcher) => {
30
+ if (typeof dispatcher.close === 'function') {
31
+ void dispatcher.close();
32
+ }
33
+ },
34
+ });
35
+ /**
36
+ * Clear the dispatcher cache. Useful for testing.
37
+ */
38
+ export function clearDispatcherCache() {
39
+ DISPATCHER_CACHE.clear();
40
+ }
41
+ /**
42
+ * Get a dispatcher for the given URI and options.
43
+ * Returns undefined if no special configuration is needed (to use global dispatcher).
44
+ */
45
+ export function getDispatcher(uri, opts) {
46
+ // If no special options are set, use the global dispatcher
47
+ if (!needsCustomDispatcher(opts)) {
48
+ return undefined;
49
+ }
50
+ const parsedUri = new URL(uri);
51
+ if ((opts.httpProxy || opts.httpsProxy) && !checkNoProxy(parsedUri, opts)) {
52
+ const proxyDispatcher = getProxyDispatcher(parsedUri, opts);
53
+ if (proxyDispatcher)
54
+ return proxyDispatcher;
55
+ }
56
+ return getNonProxyDispatcher(parsedUri, opts);
57
+ }
58
+ function hasClientCertificates(certs) {
59
+ if (!certs)
60
+ return false;
61
+ for (const uri in certs) {
62
+ const entry = certs[uri];
63
+ if (entry.cert || entry.key || entry.ca)
64
+ return true;
65
+ }
66
+ return false;
67
+ }
68
+ function needsCustomDispatcher(opts) {
69
+ return Boolean(opts.httpProxy ||
70
+ opts.httpsProxy ||
71
+ opts.ca ||
72
+ opts.cert ||
73
+ opts.key ||
74
+ opts.localAddress ||
75
+ opts.strictSsl === false ||
76
+ hasClientCertificates(opts.clientCertificates) ||
77
+ opts.maxSockets);
78
+ }
79
+ function parseProxyUrl(proxy, protocol) {
80
+ let proxyUrl = proxy;
81
+ if (!proxyUrl.includes('://')) {
82
+ proxyUrl = `${protocol}//${proxyUrl}`;
83
+ }
84
+ try {
85
+ return new URL(proxyUrl);
86
+ }
87
+ catch {
88
+ throw new PnpmError('INVALID_PROXY', "Couldn't parse proxy URL", {
89
+ hint: 'If your proxy URL contains a username and password, make sure to URL-encode them ' +
90
+ '(you may use the encodeURIComponent function). For instance, ' +
91
+ 'https-proxy=https://use%21r:pas%2As@my.proxy:1234/foo. ' +
92
+ 'Do not encode the colon (:) between the username and password.',
93
+ });
94
+ }
95
+ }
96
+ function getSocksProxyType(protocol) {
97
+ switch (protocol.replace(':', '')) {
98
+ case 'socks4':
99
+ case 'socks4a':
100
+ return 4;
101
+ default:
102
+ return 5;
103
+ }
104
+ }
105
+ function getProxyDispatcher(parsedUri, opts) {
106
+ const isHttps = parsedUri.protocol === 'https:';
107
+ const proxy = isHttps ? opts.httpsProxy : opts.httpProxy;
108
+ if (!proxy)
109
+ return null;
110
+ const proxyUrl = parseProxyUrl(proxy, parsedUri.protocol);
111
+ const sslConfig = pickSettingByUrl(opts.clientCertificates, parsedUri.href);
112
+ const { ca, cert, key: certKey } = { ...opts, ...sslConfig };
113
+ const key = [
114
+ `proxy:${proxyUrl.protocol}//${proxyUrl.username}:${proxyUrl.password}@${proxyUrl.host}:${proxyUrl.port}`,
115
+ `https:${isHttps.toString()}`,
116
+ `local-address:${opts.localAddress ?? '>no-local-address<'}`,
117
+ `max-sockets:${(opts.maxSockets ?? DEFAULT_MAX_SOCKETS).toString()}`,
118
+ `strict-ssl:${isHttps ? Boolean(opts.strictSsl).toString() : '>no-strict-ssl<'}`,
119
+ `ca:${(isHttps && ca?.toString()) || '-'}`,
120
+ `cert:${(isHttps && cert?.toString()) || '-'}`,
121
+ `key:${(isHttps && certKey?.toString()) || '-'}`,
122
+ ].join(':');
123
+ if (DISPATCHER_CACHE.has(key)) {
124
+ return DISPATCHER_CACHE.get(key);
125
+ }
126
+ let dispatcher;
127
+ if (proxyUrl.protocol.startsWith('socks')) {
128
+ dispatcher = createSocksDispatcher(proxyUrl, parsedUri, opts, { ca, cert, key: certKey });
129
+ }
130
+ else {
131
+ dispatcher = createHttpProxyDispatcher(proxyUrl, isHttps, opts, { ca, cert, key: certKey });
132
+ }
133
+ DISPATCHER_CACHE.set(key, dispatcher);
134
+ return dispatcher;
135
+ }
136
+ function createHttpProxyDispatcher(proxyUrl, isHttps, opts, tlsConfig) {
137
+ return new ProxyAgent({
138
+ uri: proxyUrl.href,
139
+ token: proxyUrl.username
140
+ ? `Basic ${Buffer.from(`${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`).toString('base64')}`
141
+ : undefined,
142
+ connections: opts.maxSockets ?? DEFAULT_MAX_SOCKETS,
143
+ keepAliveTimeout: KEEP_ALIVE_TIMEOUT,
144
+ keepAliveMaxTimeout: KEEP_ALIVE_MAX_TIMEOUT,
145
+ requestTls: isHttps
146
+ ? {
147
+ ca: tlsConfig.ca,
148
+ cert: tlsConfig.cert,
149
+ key: tlsConfig.key,
150
+ rejectUnauthorized: opts.strictSsl ?? true,
151
+ localAddress: opts.localAddress,
152
+ }
153
+ : undefined,
154
+ proxyTls: {
155
+ ca: opts.ca,
156
+ rejectUnauthorized: opts.strictSsl ?? true,
157
+ },
158
+ });
159
+ }
160
+ function createSocksDispatcher(proxyUrl, targetUri, opts, tlsConfig) {
161
+ const isHttps = targetUri.protocol === 'https:';
162
+ const socksType = getSocksProxyType(proxyUrl.protocol);
163
+ const proxyHost = proxyUrl.hostname;
164
+ const proxyPort = parseInt(proxyUrl.port, 10) || (socksType === 4 ? 1080 : 1080);
165
+ return new Agent({
166
+ connections: opts.maxSockets ?? DEFAULT_MAX_SOCKETS,
167
+ keepAliveTimeout: KEEP_ALIVE_TIMEOUT,
168
+ keepAliveMaxTimeout: KEEP_ALIVE_MAX_TIMEOUT,
169
+ connect: async (connectOpts, callback) => {
170
+ try {
171
+ const { socket } = await SocksClient.createConnection({
172
+ proxy: {
173
+ host: proxyHost,
174
+ port: proxyPort,
175
+ type: socksType,
176
+ userId: proxyUrl.username ? decodeURIComponent(proxyUrl.username) : undefined,
177
+ password: proxyUrl.password ? decodeURIComponent(proxyUrl.password) : undefined,
178
+ },
179
+ command: 'connect',
180
+ destination: {
181
+ host: connectOpts.hostname,
182
+ port: parseInt(String(connectOpts.port), 10),
183
+ },
184
+ });
185
+ if (isHttps) {
186
+ const tlsOpts = {
187
+ socket: socket,
188
+ servername: connectOpts.hostname,
189
+ ca: tlsConfig.ca,
190
+ cert: tlsConfig.cert,
191
+ key: tlsConfig.key,
192
+ rejectUnauthorized: opts.strictSsl ?? true,
193
+ };
194
+ const tlsSocket = tls.connect(tlsOpts);
195
+ tlsSocket.on('secureConnect', () => {
196
+ callback(null, tlsSocket);
197
+ });
198
+ tlsSocket.on('error', (err) => {
199
+ callback(err, null);
200
+ });
201
+ }
202
+ else {
203
+ callback(null, socket);
204
+ }
205
+ }
206
+ catch (err) {
207
+ callback(err, null);
208
+ }
209
+ },
210
+ });
211
+ }
212
+ function getNonProxyDispatcher(parsedUri, opts) {
213
+ const isHttps = parsedUri.protocol === 'https:';
214
+ const sslConfig = pickSettingByUrl(opts.clientCertificates, parsedUri.href);
215
+ const { ca, cert, key: certKey } = { ...opts, ...sslConfig };
216
+ const key = [
217
+ `https:${isHttps.toString()}`,
218
+ `local-address:${opts.localAddress ?? '>no-local-address<'}`,
219
+ `max-sockets:${(opts.maxSockets ?? DEFAULT_MAX_SOCKETS).toString()}`,
220
+ `strict-ssl:${isHttps ? Boolean(opts.strictSsl).toString() : '>no-strict-ssl<'}`,
221
+ `ca:${(isHttps && ca?.toString()) || '-'}`,
222
+ `cert:${(isHttps && cert?.toString()) || '-'}`,
223
+ `key:${(isHttps && certKey?.toString()) || '-'}`,
224
+ ].join(':');
225
+ if (DISPATCHER_CACHE.has(key)) {
226
+ return DISPATCHER_CACHE.get(key);
227
+ }
228
+ const connectTimeout = typeof opts.timeout !== 'number' || opts.timeout === 0
229
+ ? 0
230
+ : opts.timeout + 1;
231
+ const agent = new Agent({
232
+ connections: opts.maxSockets ?? DEFAULT_MAX_SOCKETS,
233
+ connectTimeout,
234
+ keepAliveTimeout: KEEP_ALIVE_TIMEOUT,
235
+ keepAliveMaxTimeout: KEEP_ALIVE_MAX_TIMEOUT,
236
+ connect: isHttps
237
+ ? {
238
+ autoSelectFamily: true,
239
+ ca,
240
+ cert,
241
+ key: certKey,
242
+ rejectUnauthorized: opts.strictSsl ?? true,
243
+ localAddress: opts.localAddress,
244
+ }
245
+ : {
246
+ autoSelectFamily: true,
247
+ localAddress: opts.localAddress,
248
+ },
249
+ });
250
+ DISPATCHER_CACHE.set(key, agent);
251
+ return agent;
252
+ }
253
+ function checkNoProxy(parsedUri, opts) {
254
+ const host = parsedUri.hostname
255
+ .split('.')
256
+ .filter(x => x)
257
+ .reverse();
258
+ if (typeof opts.noProxy === 'string') {
259
+ const noproxyArr = opts.noProxy.split(',').map(s => s.trim());
260
+ return noproxyArr.some(no => {
261
+ const noParts = no
262
+ .split('.')
263
+ .filter(x => x)
264
+ .reverse();
265
+ if (noParts.length === 0) {
266
+ return false;
267
+ }
268
+ for (let i = 0; i < noParts.length; i++) {
269
+ if (host[i] !== noParts[i]) {
270
+ return false;
271
+ }
272
+ }
273
+ return true;
274
+ });
275
+ }
276
+ return opts.noProxy === true;
277
+ }
278
+ /**
279
+ * Pick SSL/TLS configuration by URL using nerf-dart matching.
280
+ * This matches the behavior of @pnpm/network.config's pickSettingByUrl.
281
+ */
282
+ function pickSettingByUrl(settings, uri) {
283
+ if (!settings)
284
+ return undefined;
285
+ // Try exact match first
286
+ if (settings[uri])
287
+ return settings[uri];
288
+ // Use nerf-dart format for matching (e.g., //registry.npmjs.org/)
289
+ const nerf = nerfDart(uri);
290
+ if (settings[nerf])
291
+ return settings[nerf];
292
+ // Try without port
293
+ const parsedUrl = new URL(uri);
294
+ const withoutPort = removePort(parsedUrl);
295
+ if (settings[withoutPort])
296
+ return settings[withoutPort];
297
+ // Try progressively shorter nerf-dart paths
298
+ const maxParts = Object.keys(settings).reduce((max, key) => {
299
+ const parts = key.split('/').length;
300
+ return parts > max ? parts : max;
301
+ }, 0);
302
+ const parts = nerf.split('/');
303
+ for (let i = Math.min(parts.length, maxParts) - 1; i >= 3; i--) {
304
+ const key = `${parts.slice(0, i).join('/')}/`;
305
+ if (settings[key]) {
306
+ return settings[key];
307
+ }
308
+ }
309
+ // If the URL had a port, try again without it
310
+ if (withoutPort !== uri) {
311
+ return pickSettingByUrl(settings, withoutPort);
312
+ }
313
+ return undefined;
314
+ }
315
+ function removePort(parsedUrl) {
316
+ if (parsedUrl.port === '')
317
+ return parsedUrl.href;
318
+ const copy = new URL(parsedUrl.href);
319
+ copy.port = '';
320
+ const res = copy.toString();
321
+ return res.endsWith('/') ? res : `${res}/`;
322
+ }
323
+ //# sourceMappingURL=dispatcher.js.map
package/lib/fetch.d.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  import { type RetryTimeoutOptions } from '@zkochan/retry';
2
- import { type Request, type RequestInit as NodeRequestInit, Response } from 'node-fetch';
3
- export { isRedirect } from 'node-fetch';
4
- export { Response, type RetryTimeoutOptions };
2
+ import { type Dispatcher } from 'undici';
3
+ export { type RetryTimeoutOptions };
5
4
  interface URLLike {
6
5
  href: string;
7
6
  }
8
- export type RequestInfo = string | URLLike | Request;
9
- export interface RequestInit extends NodeRequestInit {
7
+ export declare function isRedirect(statusCode: number): boolean;
8
+ export type RequestInfo = string | URLLike | URL;
9
+ export interface RequestInit extends globalThis.RequestInit {
10
10
  retry?: RetryTimeoutOptions;
11
11
  timeout?: number;
12
+ dispatcher?: Dispatcher;
12
13
  }
13
14
  export declare function fetch(url: RequestInfo, opts?: RequestInit): Promise<Response>;
14
15
  export declare class ResponseError extends Error {
package/lib/fetch.js CHANGED
@@ -1,14 +1,15 @@
1
- import assert from 'node:assert';
2
- import util from 'node:util';
3
1
  import { requestRetryLogger } from '@pnpm/core-loggers';
4
2
  import { operation } from '@zkochan/retry';
5
- import nodeFetch, { Response } from 'node-fetch';
6
- export { isRedirect } from 'node-fetch';
7
- export { Response };
3
+ import { fetch as undiciFetch } from 'undici';
4
+ export {};
8
5
  const NO_RETRY_ERROR_CODES = new Set([
9
6
  'SELF_SIGNED_CERT_IN_CHAIN',
10
7
  'ERR_OSSL_PEM_NO_START_LINE',
11
8
  ]);
9
+ const REDIRECT_CODES = new Set([301, 302, 303, 307, 308]);
10
+ export function isRedirect(statusCode) {
11
+ return REDIRECT_CODES.has(statusCode);
12
+ }
12
13
  export async function fetch(url, opts = {}) {
13
14
  const retryOpts = opts.retry ?? {};
14
15
  const maxRetries = retryOpts.retries ?? 2;
@@ -22,9 +23,13 @@ export async function fetch(url, opts = {}) {
22
23
  try {
23
24
  return await new Promise((resolve, reject) => {
24
25
  op.attempt(async (attempt) => {
26
+ const urlString = typeof url === 'string' ? url : url.href ?? url.toString();
27
+ const { retry: _retry, timeout, dispatcher, ...fetchOpts } = opts;
28
+ const signal = timeout ? AbortSignal.timeout(timeout) : undefined;
25
29
  try {
26
- // this will be retried
27
- const res = await nodeFetch(url, opts); // eslint-disable-line
30
+ // undici's Response type differs slightly from globalThis.Response (iterator types),
31
+ // requiring the double cast. This is a known TypeScript/undici compatibility issue.
32
+ const res = await undiciFetch(urlString, { ...fetchOpts, signal, dispatcher });
28
33
  // A retry on 409 sometimes helps when making requests to the Bit registry.
29
34
  if ((res.status >= 500 && res.status < 600) || [408, 409, 420, 429].includes(res.status)) {
30
35
  throw new ResponseError(res);
@@ -34,24 +39,42 @@ export async function fetch(url, opts = {}) {
34
39
  }
35
40
  }
36
41
  catch (error) {
37
- assert(util.types.isNativeError(error));
38
- if ('code' in error &&
39
- typeof error.code === 'string' &&
40
- NO_RETRY_ERROR_CODES.has(error.code)) {
42
+ // Undici errors may not pass isNativeError check, so we handle them more carefully
43
+ const err = error;
44
+ // Check error code in both error.code and error.cause.code (undici wraps errors)
45
+ const errorCode = err?.code ?? err?.cause?.code;
46
+ if (typeof errorCode === 'string' &&
47
+ NO_RETRY_ERROR_CODES.has(errorCode)) {
41
48
  throw error;
42
49
  }
43
- const timeout = op.retry(error);
44
- if (timeout === false) {
50
+ const retryTimeout = op.retry(err);
51
+ if (retryTimeout === false) {
45
52
  reject(op.mainError());
46
53
  return;
47
54
  }
55
+ // Extract error properties into a plain object because Error properties
56
+ // are non-enumerable and don't serialize well through the logging system
57
+ const errorInfo = {
58
+ name: err.name,
59
+ message: err.message,
60
+ code: err.code,
61
+ errno: err.errno,
62
+ // For HTTP errors from ResponseError class
63
+ status: err.status,
64
+ statusCode: err.statusCode,
65
+ // undici wraps the actual network error in a cause property
66
+ cause: err.cause ? {
67
+ code: err.cause.code,
68
+ errno: err.cause.errno,
69
+ } : undefined,
70
+ };
48
71
  requestRetryLogger.debug({
49
72
  attempt,
50
- error,
73
+ error: errorInfo,
51
74
  maxRetries,
52
75
  method: opts.method ?? 'GET',
53
- timeout,
54
- url: url.toString(),
76
+ timeout: retryTimeout,
77
+ url: urlString,
55
78
  });
56
79
  }
57
80
  });
@@ -1,14 +1,15 @@
1
+ import { URL } from 'node:url';
1
2
  import type { FetchFromRegistry } from '@pnpm/fetching.types';
2
- import { type AgentOptions } from '@pnpm/network.agent';
3
- import type { SslConfig } from '@pnpm/types';
4
- import { type RequestInfo, type RequestInit, type Response } from './fetch.js';
5
- export interface FetchWithAgentOptions extends RequestInit {
6
- agentOptions: AgentOptions;
3
+ import type { RegistryConfig } from '@pnpm/types';
4
+ import { type DispatcherOptions } from './dispatcher.js';
5
+ import { type RequestInit } from './fetch.js';
6
+ export interface FetchWithDispatcherOptions extends RequestInit {
7
+ dispatcherOptions: DispatcherOptions;
7
8
  }
8
- export declare function fetchWithAgent(url: RequestInfo, opts: FetchWithAgentOptions): Promise<Response>;
9
- export type { AgentOptions };
10
- export interface CreateFetchFromRegistryOptions extends AgentOptions {
9
+ export declare function fetchWithDispatcher(url: string | URL, opts: FetchWithDispatcherOptions): Promise<Response>;
10
+ export type { DispatcherOptions };
11
+ export interface CreateFetchFromRegistryOptions extends DispatcherOptions {
11
12
  userAgent?: string;
12
- sslConfigs?: Record<string, SslConfig>;
13
+ configByUri?: Record<string, RegistryConfig>;
13
14
  }
14
15
  export declare function createFetchFromRegistry(defaultOpts: CreateFetchFromRegistryOptions): FetchFromRegistry;
@@ -1,5 +1,5 @@
1
1
  import { URL } from 'node:url';
2
- import { getAgent } from '@pnpm/network.agent';
2
+ import { getDispatcher } from './dispatcher.js';
3
3
  import { fetch, isRedirect } from './fetch.js';
4
4
  const USER_AGENT = 'pnpm'; // or maybe make it `${pkg.name}/${pkg.version} (+https://npm.im/${pkg.name})`
5
5
  const FULL_DOC = 'application/json';
@@ -7,17 +7,14 @@ const ACCEPT_FULL_DOC = `${FULL_DOC}; q=1.0, */*`;
7
7
  const ABBREVIATED_DOC = 'application/vnd.npm.install-v1+json';
8
8
  const ACCEPT_ABBREVIATED_DOC = `${ABBREVIATED_DOC}; q=1.0, ${FULL_DOC}; q=0.8, */*`;
9
9
  const MAX_FOLLOWED_REDIRECTS = 20;
10
- export function fetchWithAgent(url, opts) {
11
- const agent = getAgent(url.toString(), {
12
- ...opts.agentOptions,
13
- strictSsl: opts.agentOptions.strictSsl ?? true,
14
- }); // eslint-disable-line
15
- const headers = opts.headers ?? {};
16
- // @ts-expect-error
17
- headers['connection'] = agent ? 'keep-alive' : 'close';
10
+ export function fetchWithDispatcher(url, opts) {
11
+ const dispatcher = getDispatcher(url.toString(), {
12
+ ...opts.dispatcherOptions,
13
+ strictSsl: opts.dispatcherOptions.strictSsl ?? true,
14
+ });
18
15
  return fetch(url, {
19
16
  ...opts,
20
- agent,
17
+ dispatcher,
21
18
  });
22
19
  }
23
20
  export function createFetchFromRegistry(defaultOpts) {
@@ -30,27 +27,38 @@ export function createFetchFromRegistry(defaultOpts) {
30
27
  userAgent: defaultOpts.userAgent,
31
28
  }),
32
29
  };
30
+ if (opts?.ifNoneMatch) {
31
+ headers['if-none-match'] = opts.ifNoneMatch;
32
+ }
33
+ if (opts?.ifModifiedSince) {
34
+ headers['if-modified-since'] = opts.ifModifiedSince;
35
+ }
36
+ // Merge caller-provided headers (e.g. content-type, npm-otp) on top
37
+ if (opts?.headers) {
38
+ const optsHeaders = opts.headers instanceof Headers
39
+ ? Object.fromEntries(opts.headers.entries())
40
+ : Array.isArray(opts.headers)
41
+ ? Object.fromEntries(opts.headers)
42
+ : opts.headers;
43
+ Object.assign(headers, optsHeaders);
44
+ }
33
45
  let redirects = 0;
34
46
  let urlObject = new URL(url);
35
47
  const originalHost = urlObject.host;
36
48
  /* eslint-disable no-await-in-loop */
37
49
  while (true) {
38
- const agentOptions = {
50
+ const dispatcherOptions = {
39
51
  ...defaultOpts,
40
52
  ...opts,
41
53
  strictSsl: defaultOpts.strictSsl ?? true,
42
- }; // eslint-disable-line
43
- // We should pass a URL object to node-fetch till this is not resolved:
44
- // https://github.com/bitinn/node-fetch/issues/245
45
- const response = await fetchWithAgent(urlObject, {
46
- agentOptions: {
47
- ...agentOptions,
48
- clientCertificates: defaultOpts.sslConfigs,
49
- },
50
- // if verifying integrity, node-fetch must not decompress
51
- compress: opts?.compress ?? false,
52
- method: opts?.method,
54
+ clientCertificates: extractTlsConfigs(defaultOpts.configByUri),
55
+ };
56
+ const response = await fetchWithDispatcher(urlObject, {
57
+ dispatcherOptions,
58
+ body: opts?.body,
59
+ // if verifying integrity, native fetch must not decompress
53
60
  headers,
61
+ method: opts?.method,
54
62
  redirect: 'manual',
55
63
  retry: opts?.retry,
56
64
  timeout: opts?.timeout ?? 60000,
@@ -81,6 +89,18 @@ function getHeaders(opts) {
81
89
  }
82
90
  return headers;
83
91
  }
92
+ function extractTlsConfigs(configByUri) {
93
+ if (!configByUri)
94
+ return undefined;
95
+ let result;
96
+ for (const [uri, config] of Object.entries(configByUri)) {
97
+ if (config.tls) {
98
+ result ??= {};
99
+ result[uri] = config.tls;
100
+ }
101
+ }
102
+ return result;
103
+ }
84
104
  function resolveRedirectUrl(response, currentUrl) {
85
105
  const location = response.headers.get('location');
86
106
  if (!location) {
package/lib/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { fetch, type RetryTimeoutOptions } from './fetch.js';
2
- export { type AgentOptions, createFetchFromRegistry, type CreateFetchFromRegistryOptions, fetchWithAgent } from './fetchFromRegistry.js';
1
+ export { clearDispatcherCache, getDispatcher } from './dispatcher.js';
2
+ export { fetch, isRedirect, type RetryTimeoutOptions } from './fetch.js';
3
+ export { createFetchFromRegistry, type CreateFetchFromRegistryOptions, type DispatcherOptions, fetchWithDispatcher } from './fetchFromRegistry.js';
3
4
  export type { FetchFromRegistry } from '@pnpm/fetching.types';
package/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
- export { fetch } from './fetch.js';
2
- export { createFetchFromRegistry, fetchWithAgent } from './fetchFromRegistry.js';
1
+ export { clearDispatcherCache, getDispatcher } from './dispatcher.js';
2
+ export { fetch, isRedirect } from './fetch.js';
3
+ export { createFetchFromRegistry, fetchWithDispatcher } from './fetchFromRegistry.js';
3
4
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pnpm/network.fetch",
3
- "version": "1000.2.6",
4
- "description": "node-fetch with retries",
3
+ "version": "1100.0.0",
4
+ "description": "Native fetch with retries",
5
5
  "keywords": [
6
6
  "pnpm",
7
7
  "pnpm11",
@@ -26,21 +26,23 @@
26
26
  "!*.map"
27
27
  ],
28
28
  "dependencies": {
29
- "@pnpm/network.agent": "^2.0.3",
29
+ "@pnpm/config.nerf-dart": "^1.0.0",
30
30
  "@zkochan/retry": "^0.2.0",
31
- "node-fetch": "^3.3.2",
32
- "@pnpm/core-loggers": "1001.0.4",
33
- "@pnpm/fetching.types": "1000.2.0",
34
- "@pnpm/types": "1000.9.0"
31
+ "lru-cache": "^11.2.7",
32
+ "socks": "^2.8.1",
33
+ "undici": "^7.2.0",
34
+ "@pnpm/core-loggers": "1100.0.0",
35
+ "@pnpm/error": "1100.0.0",
36
+ "@pnpm/fetching.types": "1100.0.0",
37
+ "@pnpm/types": "1100.0.0"
35
38
  },
36
39
  "peerDependencies": {
37
40
  "@pnpm/logger": ">=1001.0.0 <1002.0.0"
38
41
  },
39
42
  "devDependencies": {
40
43
  "https-proxy-server-express": "0.1.2",
41
- "nock": "13.3.4",
42
- "@pnpm/logger": "1001.0.1",
43
- "@pnpm/network.fetch": "1000.2.6"
44
+ "@pnpm/network.fetch": "1100.0.0",
45
+ "@pnpm/logger": "1100.0.0"
44
46
  },
45
47
  "engines": {
46
48
  "node": ">=22.13"
@@ -50,8 +52,8 @@
50
52
  },
51
53
  "scripts": {
52
54
  "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
53
- "_test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest",
54
- "test": "pnpm run compile && pnpm run _test",
55
- "compile": "tsgo --build && pnpm run lint --fix"
55
+ "test": "pn compile && pn .test",
56
+ "compile": "tsgo --build && pn lint --fix",
57
+ ".test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest"
56
58
  }
57
59
  }