urllib 3.25.1 → 4.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.
- package/README.md +5 -16
- package/dist/commonjs/HttpAgent.d.ts +0 -1
- package/dist/commonjs/HttpAgent.js +1 -0
- package/dist/commonjs/HttpClient.d.ts +1 -3
- package/dist/commonjs/HttpClient.js +20 -43
- package/dist/commonjs/HttpClientError.js +1 -0
- package/dist/commonjs/IncomingHttpHeaders.d.ts +0 -1
- package/dist/commonjs/IncomingHttpHeaders.js +1 -0
- package/dist/commonjs/Request.d.ts +0 -4
- package/dist/commonjs/Request.js +1 -0
- package/dist/commonjs/Response.d.ts +0 -1
- package/dist/commonjs/Response.js +1 -0
- package/dist/commonjs/diagnosticsChannel.js +24 -32
- package/dist/commonjs/index.js +7 -9
- package/dist/commonjs/symbols.js +1 -0
- package/dist/commonjs/utils.d.ts +0 -1
- package/dist/commonjs/utils.js +6 -12
- package/dist/esm/HttpAgent.d.ts +0 -1
- package/dist/esm/HttpAgent.js +1 -0
- package/dist/esm/HttpClient.d.ts +1 -3
- package/dist/esm/HttpClient.js +17 -40
- package/dist/esm/HttpClientError.js +1 -0
- package/dist/esm/IncomingHttpHeaders.d.ts +0 -1
- package/dist/esm/IncomingHttpHeaders.js +1 -0
- package/dist/esm/Request.d.ts +0 -4
- package/dist/esm/Request.js +1 -0
- package/dist/esm/Response.d.ts +0 -1
- package/dist/esm/Response.js +1 -0
- package/dist/esm/diagnosticsChannel.js +23 -30
- package/dist/esm/index.js +2 -1
- package/dist/esm/symbols.js +1 -0
- package/dist/esm/utils.d.ts +0 -1
- package/dist/esm/utils.js +1 -5
- package/package.json +18 -23
- package/src/HttpClient.ts +18 -40
- package/src/diagnosticsChannel.ts +31 -35
- package/src/index.ts +1 -1
- package/src/utils.ts +1 -7
package/src/HttpClient.ts
CHANGED
@@ -11,34 +11,33 @@ import {
|
|
11
11
|
} from 'node:zlib';
|
12
12
|
import { Blob } from 'node:buffer';
|
13
13
|
import { Readable, pipeline } from 'node:stream';
|
14
|
-
import
|
14
|
+
import { pipeline as pipelinePromise } from 'node:stream/promises';
|
15
15
|
import { basename } from 'node:path';
|
16
16
|
import { createReadStream } from 'node:fs';
|
17
17
|
import { format as urlFormat } from 'node:url';
|
18
18
|
import { performance } from 'node:perf_hooks';
|
19
19
|
import querystring from 'node:querystring';
|
20
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
20
21
|
import {
|
21
|
-
FormData
|
22
|
+
FormData,
|
22
23
|
request as undiciRequest,
|
23
24
|
Dispatcher,
|
24
25
|
Agent,
|
25
26
|
getGlobalDispatcher,
|
26
27
|
Pool,
|
27
28
|
} from 'undici';
|
29
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
30
|
+
// @ts-ignore
|
28
31
|
import undiciSymbols from 'undici/lib/core/symbols.js';
|
29
|
-
import { FormData as FormDataNode } from 'formdata-node';
|
30
|
-
import { FormDataEncoder } from 'form-data-encoder';
|
31
|
-
import createUserAgent from 'default-user-agent';
|
32
32
|
import mime from 'mime-types';
|
33
33
|
import qs from 'qs';
|
34
|
-
import pump from 'pump';
|
35
34
|
// Compatible with old style formstream
|
36
35
|
import FormStream from 'formstream';
|
37
36
|
import { HttpAgent, CheckAddressFunction } from './HttpAgent.js';
|
38
37
|
import type { IncomingHttpHeaders } from './IncomingHttpHeaders.js';
|
39
38
|
import { RequestURL, RequestOptions, HttpMethod, RequestMeta } from './Request.js';
|
40
39
|
import { RawResponseWithMeta, HttpClientResponse, SocketInfo } from './Response.js';
|
41
|
-
import { parseJSON,
|
40
|
+
import { parseJSON, digestAuthHeader, globalId, performanceTime, isReadable } from './utils.js';
|
42
41
|
import symbols from './symbols.js';
|
43
42
|
import { initDiagnosticsChannel } from './diagnosticsChannel.js';
|
44
43
|
import { HttpClientConnectTimeoutError, HttpClientRequestTimeoutError } from './HttpClientError.js';
|
@@ -49,24 +48,12 @@ type PropertyShouldBe<T, K extends keyof T, V> = Omit<T, K> & { [P in K]: V };
|
|
49
48
|
type IUndiciRequestOption = PropertyShouldBe<UndiciRequestOption, 'headers', IncomingHttpHeaders>;
|
50
49
|
|
51
50
|
const PROTO_RE = /^https?:\/\//i;
|
52
|
-
const FormData = FormDataNative ?? FormDataNode;
|
53
|
-
// impl promise pipeline on Node.js 14
|
54
|
-
const pipelinePromise = stream.promises?.pipeline ?? function pipeline(...args: any[]) {
|
55
|
-
return new Promise<void>((resolve, reject) => {
|
56
|
-
pump(...args, (err?: Error) => {
|
57
|
-
if (err) return reject(err);
|
58
|
-
resolve();
|
59
|
-
});
|
60
|
-
});
|
61
|
-
};
|
62
51
|
|
63
52
|
function noop() {
|
64
53
|
// noop
|
65
54
|
}
|
66
55
|
|
67
56
|
const debug = debuglog('urllib:HttpClient');
|
68
|
-
// Node.js 14 or 16
|
69
|
-
const isNode14Or16 = /v1[46]\./.test(process.version);
|
70
57
|
|
71
58
|
export type ClientOptions = {
|
72
59
|
defaultArgs?: RequestOptions;
|
@@ -125,7 +112,10 @@ class BlobFromStream {
|
|
125
112
|
}
|
126
113
|
}
|
127
114
|
|
128
|
-
export const
|
115
|
+
export const VERSION = 'VERSION';
|
116
|
+
// 'node-urllib/4.0.0 Node.js/18.19.0 (darwin; x64)'
|
117
|
+
export const HEADER_USER_AGENT =
|
118
|
+
`node-urllib/${VERSION} Node.js/${process.version.substring(1)} (${process.platform}; ${process.arch})`;
|
129
119
|
|
130
120
|
function getFileName(stream: Readable) {
|
131
121
|
const filePath: string = (stream as any).path;
|
@@ -207,13 +197,13 @@ export class HttpClient extends EventEmitter {
|
|
207
197
|
getDispatcherPoolStats() {
|
208
198
|
const agent = this.getDispatcher();
|
209
199
|
// origin => Pool Instance
|
210
|
-
const clients: Map<string, WeakRef<Pool>> | undefined = agent
|
200
|
+
const clients: Map<string, WeakRef<Pool>> | undefined = Reflect.get(agent, undiciSymbols.kClients);
|
211
201
|
const poolStatsMap: Record<string, PoolStat> = {};
|
212
202
|
if (!clients) {
|
213
203
|
return poolStatsMap;
|
214
204
|
}
|
215
205
|
for (const [ key, ref ] of clients) {
|
216
|
-
const pool = ref.deref();
|
206
|
+
const pool = typeof ref.deref === 'function' ? ref.deref() : ref as unknown as Pool;
|
217
207
|
const stats = pool?.stats;
|
218
208
|
if (!stats) continue;
|
219
209
|
poolStatsMap[key] = {
|
@@ -451,9 +441,11 @@ export class HttpClient extends EventEmitter {
|
|
451
441
|
} else if (typeof args.files === 'string' || Buffer.isBuffer(args.files)) {
|
452
442
|
uploadFiles.push([ 'file', args.files ]);
|
453
443
|
} else if (typeof args.files === 'object') {
|
454
|
-
|
444
|
+
const files = args.files as Record<string, string | Readable | Buffer>;
|
445
|
+
for (const field in files) {
|
455
446
|
// set custom fileName
|
456
|
-
|
447
|
+
const file = files[field];
|
448
|
+
uploadFiles.push([ field, file, field ]);
|
457
449
|
}
|
458
450
|
}
|
459
451
|
// set normal fields first
|
@@ -478,18 +470,7 @@ export class HttpClient extends EventEmitter {
|
|
478
470
|
isStreamingRequest = true;
|
479
471
|
}
|
480
472
|
}
|
481
|
-
|
482
|
-
if (FormDataNative) {
|
483
|
-
requestOptions.body = formData;
|
484
|
-
} else {
|
485
|
-
// Node.js 14 does not support spec-compliant FormData
|
486
|
-
// https://github.com/octet-stream/form-data#usage
|
487
|
-
const encoder = new FormDataEncoder(formData as any);
|
488
|
-
Object.assign(headers, encoder.headers);
|
489
|
-
// fix "Content-Length":"NaN"
|
490
|
-
delete headers['Content-Length'];
|
491
|
-
requestOptions.body = Readable.from(encoder);
|
492
|
-
}
|
473
|
+
requestOptions.body = formData;
|
493
474
|
} else if (args.content) {
|
494
475
|
if (!isGETOrHEAD) {
|
495
476
|
// handle content
|
@@ -507,7 +488,7 @@ export class HttpClient extends EventEmitter {
|
|
507
488
|
|| isReadable(args.data);
|
508
489
|
if (isGETOrHEAD) {
|
509
490
|
if (!isStringOrBufferOrReadable) {
|
510
|
-
let query;
|
491
|
+
let query: string;
|
511
492
|
if (args.nestedQuerystring) {
|
512
493
|
query = qs.stringify(args.data);
|
513
494
|
} else {
|
@@ -608,9 +589,6 @@ export class HttpClient extends EventEmitter {
|
|
608
589
|
res = Object.assign(response.body, res);
|
609
590
|
}
|
610
591
|
} else if (args.writeStream) {
|
611
|
-
if (isNode14Or16 && args.writeStream.destroyed) {
|
612
|
-
throw new Error('writeStream is destroyed');
|
613
|
-
}
|
614
592
|
if (args.compressed === true && isCompressedContent) {
|
615
593
|
const decoder = contentEncoding === 'gzip' ? createGunzip() : createBrotliDecompress();
|
616
594
|
await pipelinePromise(response.body, decoder, args.writeStream);
|
@@ -20,15 +20,14 @@ let initedDiagnosticsChannel = false;
|
|
20
20
|
// -> undici:request:trailers => { request, trailers }
|
21
21
|
|
22
22
|
function subscribe(name: string, listener: (message: unknown, channelName: string | symbol) => void) {
|
23
|
-
|
24
|
-
diagnosticsChannel.subscribe(name, listener);
|
25
|
-
} else {
|
26
|
-
// TODO: support Node.js 14, will be removed on the next major version
|
27
|
-
diagnosticsChannel.channel(name).subscribe(listener);
|
28
|
-
}
|
23
|
+
diagnosticsChannel.subscribe(name, listener);
|
29
24
|
}
|
30
25
|
|
31
|
-
|
26
|
+
type SocketExtend = Socket & {
|
27
|
+
[key: symbol]: string | number | Date | undefined;
|
28
|
+
};
|
29
|
+
|
30
|
+
function formatSocket(socket: SocketExtend) {
|
32
31
|
if (!socket) return socket;
|
33
32
|
return {
|
34
33
|
localAddress: socket[symbols.kSocketLocalAddress],
|
@@ -41,8 +40,7 @@ function formatSocket(socket: Socket) {
|
|
41
40
|
}
|
42
41
|
|
43
42
|
// make sure error contains socket info
|
44
|
-
const
|
45
|
-
Socket.prototype[kDestroy] = Socket.prototype.destroy;
|
43
|
+
const destroySocket = Socket.prototype.destroy;
|
46
44
|
Socket.prototype.destroy = function(err?: any) {
|
47
45
|
if (err) {
|
48
46
|
Object.defineProperty(err, symbols.kErrorSocket, {
|
@@ -51,12 +49,12 @@ Socket.prototype.destroy = function(err?: any) {
|
|
51
49
|
value: this,
|
52
50
|
});
|
53
51
|
}
|
54
|
-
return this
|
52
|
+
return destroySocket.call(this, err);
|
55
53
|
};
|
56
54
|
|
57
55
|
function getRequestOpaque(request: DiagnosticsChannel.Request, kHandler?: symbol) {
|
58
56
|
if (!kHandler) return;
|
59
|
-
const handler = request
|
57
|
+
const handler = Reflect.get(request, kHandler);
|
60
58
|
// maxRedirects = 0 will get [Symbol(handler)]: RequestHandler {
|
61
59
|
// responseHeaders: null,
|
62
60
|
// opaque: {
|
@@ -70,7 +68,7 @@ function getRequestOpaque(request: DiagnosticsChannel.Request, kHandler?: symbol
|
|
70
68
|
}
|
71
69
|
|
72
70
|
export function initDiagnosticsChannel() {
|
73
|
-
//
|
71
|
+
// make sure init global DiagnosticsChannel once
|
74
72
|
if (initedDiagnosticsChannel) return;
|
75
73
|
initedDiagnosticsChannel = true;
|
76
74
|
|
@@ -97,29 +95,27 @@ export function initDiagnosticsChannel() {
|
|
97
95
|
opaque[symbols.kRequestTiming].queuing = performanceTime(opaque[symbols.kRequestStartTime]);
|
98
96
|
});
|
99
97
|
|
100
|
-
// diagnosticsChannel.channel('undici:client:beforeConnect')
|
101
|
-
|
102
98
|
subscribe('undici:client:connectError', (message, name) => {
|
103
|
-
const { error, connectParams } = message as DiagnosticsChannel.ClientConnectErrorMessage & { error: any };
|
104
|
-
let
|
105
|
-
if (!
|
106
|
-
|
99
|
+
const { error, connectParams, socket } = message as DiagnosticsChannel.ClientConnectErrorMessage & { error: any, socket: SocketExtend };
|
100
|
+
let sock = socket;
|
101
|
+
if (!sock && error[symbols.kErrorSocket]) {
|
102
|
+
sock = error[symbols.kErrorSocket];
|
107
103
|
}
|
108
|
-
if (
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
104
|
+
if (sock) {
|
105
|
+
sock[symbols.kSocketId] = globalId('UndiciSocket');
|
106
|
+
sock[symbols.kSocketConnectErrorTime] = new Date();
|
107
|
+
sock[symbols.kHandledRequests] = 0;
|
108
|
+
sock[symbols.kHandledResponses] = 0;
|
113
109
|
// copy local address to symbol, avoid them be reset after request error throw
|
114
|
-
if (
|
115
|
-
|
116
|
-
|
110
|
+
if (sock.localAddress) {
|
111
|
+
sock[symbols.kSocketLocalAddress] = sock.localAddress;
|
112
|
+
sock[symbols.kSocketLocalPort] = sock.localPort;
|
117
113
|
}
|
118
|
-
|
119
|
-
|
120
|
-
|
114
|
+
sock[symbols.kSocketConnectProtocol] = connectParams.protocol;
|
115
|
+
sock[symbols.kSocketConnectHost] = connectParams.host;
|
116
|
+
sock[symbols.kSocketConnectPort] = connectParams.port;
|
121
117
|
debug('[%s] Socket#%d connectError, connectParams: %o, error: %s, (sock: %o)',
|
122
|
-
name,
|
118
|
+
name, sock[symbols.kSocketId], connectParams, (error as Error).message, formatSocket(sock));
|
123
119
|
} else {
|
124
120
|
debug('[%s] connectError, connectParams: %o, error: %o',
|
125
121
|
name, connectParams, error);
|
@@ -128,7 +124,7 @@ export function initDiagnosticsChannel() {
|
|
128
124
|
|
129
125
|
// This message is published after a connection is established.
|
130
126
|
subscribe('undici:client:connected', (message, name) => {
|
131
|
-
const { socket, connectParams } = message as DiagnosticsChannel.ClientConnectedMessage;
|
127
|
+
const { socket, connectParams } = message as DiagnosticsChannel.ClientConnectedMessage & { socket: SocketExtend };
|
132
128
|
socket[symbols.kSocketId] = globalId('UndiciSocket');
|
133
129
|
socket[symbols.kSocketStartTime] = performance.now();
|
134
130
|
socket[symbols.kSocketConnectedTime] = new Date();
|
@@ -145,11 +141,11 @@ export function initDiagnosticsChannel() {
|
|
145
141
|
|
146
142
|
// This message is published right before the first byte of the request is written to the socket.
|
147
143
|
subscribe('undici:client:sendHeaders', (message, name) => {
|
148
|
-
const { request, socket } = message as DiagnosticsChannel.ClientSendHeadersMessage;
|
144
|
+
const { request, socket } = message as DiagnosticsChannel.ClientSendHeadersMessage & { socket: SocketExtend };
|
149
145
|
const opaque = getRequestOpaque(request, kHandler);
|
150
146
|
if (!opaque || !opaque[symbols.kRequestId]) return;
|
151
147
|
|
152
|
-
socket[symbols.kHandledRequests]++;
|
148
|
+
(socket[symbols.kHandledRequests] as number)++;
|
153
149
|
// attach socket to opaque
|
154
150
|
opaque[symbols.kRequestSocket] = socket;
|
155
151
|
debug('[%s] Request#%d send headers on Socket#%d (handled %d requests, sock: %o)',
|
@@ -158,11 +154,11 @@ export function initDiagnosticsChannel() {
|
|
158
154
|
|
159
155
|
if (!opaque[symbols.kEnableRequestTiming]) return;
|
160
156
|
opaque[symbols.kRequestTiming].requestHeadersSent = performanceTime(opaque[symbols.kRequestStartTime]);
|
161
|
-
// first socket need to
|
157
|
+
// first socket need to calculate the connected time
|
162
158
|
if (socket[symbols.kHandledRequests] === 1) {
|
163
159
|
// kSocketStartTime - kRequestStartTime = connected time
|
164
160
|
opaque[symbols.kRequestTiming].connected =
|
165
|
-
performanceTime(opaque[symbols.kRequestStartTime], socket[symbols.kSocketStartTime]);
|
161
|
+
performanceTime(opaque[symbols.kRequestStartTime], socket[symbols.kSocketStartTime] as number);
|
166
162
|
}
|
167
163
|
});
|
168
164
|
|
package/src/index.ts
CHANGED
package/src/utils.ts
CHANGED
@@ -3,7 +3,7 @@ import { Readable } from 'node:stream';
|
|
3
3
|
import { performance } from 'node:perf_hooks';
|
4
4
|
import type { FixJSONCtlChars } from './Request.js';
|
5
5
|
|
6
|
-
const JSONCtlCharsMap = {
|
6
|
+
const JSONCtlCharsMap: Record<string, string> = {
|
7
7
|
'"': '\\"', // \u0022
|
8
8
|
'\\': '\\\\', // \u005c
|
9
9
|
'\b': '\\b', // \u0008
|
@@ -49,12 +49,6 @@ export function parseJSON(data: string, fixJSONCtlChars?: FixJSONCtlChars) {
|
|
49
49
|
return data;
|
50
50
|
}
|
51
51
|
|
52
|
-
export function sleep(ms: number) {
|
53
|
-
return new Promise<void>(resolve => {
|
54
|
-
setTimeout(resolve, ms);
|
55
|
-
});
|
56
|
-
}
|
57
|
-
|
58
52
|
function md5(s: string) {
|
59
53
|
const sum = createHash('md5');
|
60
54
|
sum.update(s, 'utf8');
|