@pnpm/network.fetch 1000.2.6 → 1100.0.1
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.
- package/lib/dispatcher.d.ts +25 -0
- package/lib/dispatcher.js +323 -0
- package/lib/fetch.d.ts +6 -5
- package/lib/fetch.js +39 -16
- package/lib/fetchFromRegistry.d.ts +10 -9
- package/lib/fetchFromRegistry.js +42 -22
- package/lib/index.d.ts +3 -2
- package/lib/index.js +3 -2
- package/package.json +15 -13
|
@@ -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
|
|
3
|
-
export {
|
|
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
|
|
9
|
-
export
|
|
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
|
|
6
|
-
export {
|
|
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
|
-
//
|
|
27
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
44
|
-
if (
|
|
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:
|
|
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 {
|
|
3
|
-
import type
|
|
4
|
-
import { type
|
|
5
|
-
export interface
|
|
6
|
-
|
|
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
|
|
9
|
-
export type {
|
|
10
|
-
export interface CreateFetchFromRegistryOptions extends
|
|
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
|
-
|
|
13
|
+
configByUri?: Record<string, RegistryConfig>;
|
|
13
14
|
}
|
|
14
15
|
export declare function createFetchFromRegistry(defaultOpts: CreateFetchFromRegistryOptions): FetchFromRegistry;
|
package/lib/fetchFromRegistry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { URL } from 'node:url';
|
|
2
|
-
import {
|
|
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
|
|
11
|
-
const
|
|
12
|
-
...opts.
|
|
13
|
-
strictSsl: opts.
|
|
14
|
-
});
|
|
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
|
-
|
|
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
|
|
50
|
+
const dispatcherOptions = {
|
|
39
51
|
...defaultOpts,
|
|
40
52
|
...opts,
|
|
41
53
|
strictSsl: defaultOpts.strictSsl ?? true,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 {
|
|
2
|
-
export {
|
|
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 {
|
|
2
|
-
export {
|
|
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": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1100.0.1",
|
|
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/
|
|
29
|
+
"@pnpm/config.nerf-dart": "^1.0.0",
|
|
30
30
|
"@zkochan/retry": "^0.2.0",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"@pnpm/
|
|
31
|
+
"lru-cache": "^11.2.7",
|
|
32
|
+
"socks": "^2.8.1",
|
|
33
|
+
"undici": "^7.2.0",
|
|
34
|
+
"@pnpm/core-loggers": "1100.0.1",
|
|
35
|
+
"@pnpm/error": "1100.0.0",
|
|
36
|
+
"@pnpm/types": "1101.0.0",
|
|
37
|
+
"@pnpm/fetching.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
|
-
"
|
|
42
|
-
"@pnpm/
|
|
43
|
-
"@pnpm/network.fetch": "1000.2.6"
|
|
44
|
+
"@pnpm/logger": "1100.0.0",
|
|
45
|
+
"@pnpm/network.fetch": "1100.0.1"
|
|
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
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
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
|
}
|