urllib 3.16.1 → 3.17.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 +1 -4
- package/package.json +5 -3
- package/src/HttpClient.ts +57 -6
- package/src/Request.ts +9 -1
- package/src/Response.ts +2 -0
- package/src/cjs/HttpClient.d.ts +10 -2
- package/src/cjs/HttpClient.js +43 -4
- package/src/cjs/Request.d.ts +8 -1
- package/src/cjs/Response.d.ts +2 -0
- package/src/cjs/diagnosticsChannel.js +42 -10
- package/src/cjs/index.d.ts +1 -1
- package/src/cjs/symbols.d.ts +4 -0
- package/src/cjs/symbols.js +4 -0
- package/src/diagnosticsChannel.ts +49 -12
- package/src/esm/HttpClient.d.ts +10 -2
- package/src/esm/HttpClient.js +43 -4
- package/src/esm/Request.d.ts +8 -1
- package/src/esm/Response.d.ts +2 -0
- package/src/esm/diagnosticsChannel.js +42 -10
- package/src/esm/index.d.ts +1 -1
- package/src/esm/index.js +1 -1
- package/src/esm/symbols.d.ts +4 -0
- package/src/esm/symbols.js +4 -0
- package/src/index.ts +4 -1
- package/src/symbols.ts +4 -0
package/README.md
CHANGED
@@ -200,10 +200,7 @@ Response is normal object, it contains:
|
|
200
200
|
- `aborted`: response was aborted or not
|
201
201
|
- `rt`: total request and response time in ms.
|
202
202
|
- `timing`: timing object if timing enable.
|
203
|
-
- `
|
204
|
-
- `remotePort`: http server ip port
|
205
|
-
- `socketHandledRequests`: socket already handled request count
|
206
|
-
- `socketHandledResponses`: socket already handled response count
|
203
|
+
- `socket`: socket info
|
207
204
|
|
208
205
|
## Run test with debug log
|
209
206
|
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "urllib",
|
3
|
-
"version": "3.
|
3
|
+
"version": "3.17.0",
|
4
4
|
"publishConfig": {
|
5
5
|
"tag": "latest"
|
6
6
|
},
|
@@ -53,6 +53,7 @@
|
|
53
53
|
"build:test": "npm run build && npm run build:cjs:test && npm run build:esm:test && npm run test-tsc",
|
54
54
|
"test-tsc": "tsc -p ./test/fixtures/ts/tsconfig.json",
|
55
55
|
"test": "npm run lint && vitest run",
|
56
|
+
"test-keepalive": "cross-env TEST_KEEPALIVE_COUNT=50 vitest run --test-timeout 120000 keep-alive-header.test.ts",
|
56
57
|
"cov": "vitest run --coverage",
|
57
58
|
"ci": "npm run lint && npm run cov && npm run build:test",
|
58
59
|
"contributor": "git-contributor",
|
@@ -78,8 +79,9 @@
|
|
78
79
|
"@types/pump": "^1.1.1",
|
79
80
|
"@types/selfsigned": "^2.0.1",
|
80
81
|
"@types/tar-stream": "^2.2.2",
|
81
|
-
"@vitest/coverage-
|
82
|
+
"@vitest/coverage-v8": "^0.32.0",
|
82
83
|
"busboy": "^1.6.0",
|
84
|
+
"cross-env": "^7.0.3",
|
83
85
|
"eslint": "^8.25.0",
|
84
86
|
"eslint-config-egg": "^12.1.0",
|
85
87
|
"git-contributor": "^2.0.0",
|
@@ -88,7 +90,7 @@
|
|
88
90
|
"selfsigned": "^2.0.1",
|
89
91
|
"tar-stream": "^2.2.0",
|
90
92
|
"typescript": "^5.0.4",
|
91
|
-
"vitest": "^0.
|
93
|
+
"vitest": "^0.32.0"
|
92
94
|
},
|
93
95
|
"engines": {
|
94
96
|
"node": ">= 14.19.3"
|
package/src/HttpClient.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import diagnosticsChannel from 'node:diagnostics_channel';
|
1
2
|
import { EventEmitter } from 'node:events';
|
2
3
|
import { LookupFunction } from 'node:net';
|
3
4
|
import { STATUS_CODES } from 'node:http';
|
@@ -29,7 +30,7 @@ import pump from 'pump';
|
|
29
30
|
// Compatible with old style formstream
|
30
31
|
import FormStream from 'formstream';
|
31
32
|
import { HttpAgent, CheckAddressFunction } from './HttpAgent';
|
32
|
-
import { RequestURL, RequestOptions, HttpMethod } from './Request';
|
33
|
+
import { RequestURL, RequestOptions, HttpMethod, RequestMeta } from './Request';
|
33
34
|
import { RawResponseWithMeta, HttpClientResponse, SocketInfo } from './Response';
|
34
35
|
import { parseJSON, sleep, digestAuthHeader, globalId, performanceTime, isReadable } from './utils';
|
35
36
|
import symbols from './symbols';
|
@@ -137,8 +138,25 @@ function defaultIsRetry(response: HttpClientResponse) {
|
|
137
138
|
|
138
139
|
type RequestContext = {
|
139
140
|
retries: number;
|
141
|
+
requestStartTime?: number;
|
140
142
|
};
|
141
143
|
|
144
|
+
const channels = {
|
145
|
+
request: diagnosticsChannel.channel('urllib:request'),
|
146
|
+
response: diagnosticsChannel.channel('urllib:response'),
|
147
|
+
};
|
148
|
+
|
149
|
+
export type RequestDiagnosticsMessage = {
|
150
|
+
request: RequestMeta;
|
151
|
+
};
|
152
|
+
|
153
|
+
export type ResponseDiagnosticsMessage = {
|
154
|
+
request: RequestMeta;
|
155
|
+
response: RawResponseWithMeta;
|
156
|
+
error?: Error;
|
157
|
+
};
|
158
|
+
|
159
|
+
|
142
160
|
export class HttpClient extends EventEmitter {
|
143
161
|
#defaultArgs?: RequestOptions;
|
144
162
|
#dispatcher?: Dispatcher;
|
@@ -188,6 +206,7 @@ export class HttpClient extends EventEmitter {
|
|
188
206
|
const headers: IncomingHttpHeaders = {};
|
189
207
|
const args = {
|
190
208
|
retry: 0,
|
209
|
+
timing: true,
|
191
210
|
...this.#defaultArgs,
|
192
211
|
...options,
|
193
212
|
// keep method and headers exists on args for request event handler to easy use
|
@@ -198,7 +217,10 @@ export class HttpClient extends EventEmitter {
|
|
198
217
|
retries: 0,
|
199
218
|
...requestContext,
|
200
219
|
};
|
201
|
-
|
220
|
+
if (!requestContext.requestStartTime) {
|
221
|
+
requestContext.requestStartTime = performance.now();
|
222
|
+
}
|
223
|
+
const requestStartTime = requestContext.requestStartTime;
|
202
224
|
|
203
225
|
// https://developer.chrome.com/docs/devtools/network/reference/?utm_source=devtools#timing-explanation
|
204
226
|
const timing = {
|
@@ -232,8 +254,8 @@ export class HttpClient extends EventEmitter {
|
|
232
254
|
args,
|
233
255
|
ctx: args.ctx,
|
234
256
|
retries: requestContext.retries,
|
235
|
-
};
|
236
|
-
const socketInfo = {
|
257
|
+
} as RequestMeta;
|
258
|
+
const socketInfo: SocketInfo = {
|
237
259
|
id: 0,
|
238
260
|
localAddress: '',
|
239
261
|
localPort: 0,
|
@@ -438,6 +460,9 @@ export class HttpClient extends EventEmitter {
|
|
438
460
|
debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s',
|
439
461
|
requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout);
|
440
462
|
requestOptions.headers = headers;
|
463
|
+
channels.request.publish({
|
464
|
+
request: reqMeta,
|
465
|
+
} as RequestDiagnosticsMessage);
|
441
466
|
if (this.listenerCount('request') > 0) {
|
442
467
|
this.emit('request', reqMeta);
|
443
468
|
}
|
@@ -556,6 +581,10 @@ export class HttpClient extends EventEmitter {
|
|
556
581
|
}
|
557
582
|
}
|
558
583
|
|
584
|
+
channels.response.publish({
|
585
|
+
request: reqMeta,
|
586
|
+
response: res,
|
587
|
+
} as ResponseDiagnosticsMessage);
|
559
588
|
if (this.listenerCount('response') > 0) {
|
560
589
|
this.emit('response', {
|
561
590
|
requestId,
|
@@ -577,11 +606,25 @@ export class HttpClient extends EventEmitter {
|
|
577
606
|
err = new HttpClientRequestTimeoutError(headersTimeout, { cause: e });
|
578
607
|
} else if (err.name === 'BodyTimeoutError') {
|
579
608
|
err = new HttpClientRequestTimeoutError(bodyTimeout, { cause: e });
|
609
|
+
} else if (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET') {
|
610
|
+
// auto retry on socket error, https://github.com/node-modules/urllib/issues/454
|
611
|
+
if (args.retry > 0 && requestContext.retries < args.retry) {
|
612
|
+
if (args.retryDelay) {
|
613
|
+
await sleep(args.retryDelay);
|
614
|
+
}
|
615
|
+
requestContext.retries++;
|
616
|
+
return await this.#requestInternal(url, options, requestContext);
|
617
|
+
}
|
580
618
|
}
|
581
619
|
err.opaque = orginalOpaque;
|
582
620
|
err.status = res.status;
|
583
621
|
err.headers = res.headers;
|
584
622
|
err.res = res;
|
623
|
+
if (err.socket) {
|
624
|
+
// store rawSocket
|
625
|
+
err._rawSocket = err.socket;
|
626
|
+
}
|
627
|
+
err.socket = socketInfo;
|
585
628
|
// make sure requestUrls not empty
|
586
629
|
if (res.requestUrls.length === 0) {
|
587
630
|
res.requestUrls.push(requestUrl.href);
|
@@ -589,6 +632,11 @@ export class HttpClient extends EventEmitter {
|
|
589
632
|
res.rt = performanceTime(requestStartTime);
|
590
633
|
this.#updateSocketInfo(socketInfo, internalOpaque);
|
591
634
|
|
635
|
+
channels.response.publish({
|
636
|
+
request: reqMeta,
|
637
|
+
response: res,
|
638
|
+
error: err,
|
639
|
+
} as ResponseDiagnosticsMessage);
|
592
640
|
if (this.listenerCount('response') > 0) {
|
593
641
|
this.emit('response', {
|
594
642
|
requestId,
|
@@ -611,13 +659,16 @@ export class HttpClient extends EventEmitter {
|
|
611
659
|
socketInfo.id = socket[symbols.kSocketId];
|
612
660
|
socketInfo.handledRequests = socket[symbols.kHandledRequests];
|
613
661
|
socketInfo.handledResponses = socket[symbols.kHandledResponses];
|
614
|
-
socketInfo.localAddress = socket.
|
615
|
-
socketInfo.localPort = socket.
|
662
|
+
socketInfo.localAddress = socket[symbols.kSocketLocalAddress];
|
663
|
+
socketInfo.localPort = socket[symbols.kSocketLocalPort];
|
616
664
|
socketInfo.remoteAddress = socket.remoteAddress;
|
617
665
|
socketInfo.remotePort = socket.remotePort;
|
618
666
|
socketInfo.remoteFamily = socket.remoteFamily;
|
619
667
|
socketInfo.bytesRead = socket.bytesRead;
|
620
668
|
socketInfo.bytesWritten = socket.bytesWritten;
|
669
|
+
socketInfo.connectedTime = socket[symbols.kSocketConnectedTime];
|
670
|
+
socketInfo.lastRequestEndTime = socket[symbols.kSocketRequestEndTime];
|
671
|
+
socket[symbols.kSocketRequestEndTime] = new Date();
|
621
672
|
}
|
622
673
|
}
|
623
674
|
}
|
package/src/Request.ts
CHANGED
@@ -99,7 +99,7 @@ export type RequestOptions = {
|
|
99
99
|
* */
|
100
100
|
gzip?: boolean;
|
101
101
|
/**
|
102
|
-
* Enable timing or not, default is
|
102
|
+
* Enable timing or not, default is true.
|
103
103
|
* */
|
104
104
|
timing?: boolean;
|
105
105
|
/**
|
@@ -134,3 +134,11 @@ export type RequestOptions = {
|
|
134
134
|
/** Default: `64 KiB` */
|
135
135
|
highWaterMark?: number;
|
136
136
|
};
|
137
|
+
|
138
|
+
export type RequestMeta = {
|
139
|
+
requestId: number;
|
140
|
+
url: string;
|
141
|
+
args: RequestOptions;
|
142
|
+
ctx?: unknown;
|
143
|
+
retries: number;
|
144
|
+
};
|
package/src/Response.ts
CHANGED
package/src/cjs/HttpClient.d.ts
CHANGED
@@ -4,8 +4,8 @@
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
5
5
|
import { LookupFunction } from 'node:net';
|
6
6
|
import { CheckAddressFunction } from './HttpAgent';
|
7
|
-
import { RequestURL, RequestOptions } from './Request';
|
8
|
-
import { HttpClientResponse } from './Response';
|
7
|
+
import { RequestURL, RequestOptions, RequestMeta } from './Request';
|
8
|
+
import { RawResponseWithMeta, HttpClientResponse } from './Response';
|
9
9
|
export type ClientOptions = {
|
10
10
|
defaultArgs?: RequestOptions;
|
11
11
|
/**
|
@@ -37,6 +37,14 @@ export type ClientOptions = {
|
|
37
37
|
};
|
38
38
|
};
|
39
39
|
export declare const HEADER_USER_AGENT: string;
|
40
|
+
export type RequestDiagnosticsMessage = {
|
41
|
+
request: RequestMeta;
|
42
|
+
};
|
43
|
+
export type ResponseDiagnosticsMessage = {
|
44
|
+
request: RequestMeta;
|
45
|
+
response: RawResponseWithMeta;
|
46
|
+
error?: Error;
|
47
|
+
};
|
40
48
|
export declare class HttpClient extends EventEmitter {
|
41
49
|
#private;
|
42
50
|
constructor(clientOptions?: ClientOptions);
|
package/src/cjs/HttpClient.js
CHANGED
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
4
|
};
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
6
6
|
exports.HttpClient = exports.HEADER_USER_AGENT = void 0;
|
7
|
+
const node_diagnostics_channel_1 = __importDefault(require("node:diagnostics_channel"));
|
7
8
|
const node_events_1 = require("node:events");
|
8
9
|
const node_http_1 = require("node:http");
|
9
10
|
const node_util_1 = require("node:util");
|
@@ -69,7 +70,7 @@ class HttpClientRequestTimeoutError extends Error {
|
|
69
70
|
Error.captureStackTrace(this, this.constructor);
|
70
71
|
}
|
71
72
|
}
|
72
|
-
exports.HEADER_USER_AGENT = (0, default_user_agent_1.default)('node-urllib', '3.
|
73
|
+
exports.HEADER_USER_AGENT = (0, default_user_agent_1.default)('node-urllib', '3.17.0');
|
73
74
|
function getFileName(stream) {
|
74
75
|
const filePath = stream.path;
|
75
76
|
if (filePath) {
|
@@ -80,6 +81,10 @@ function getFileName(stream) {
|
|
80
81
|
function defaultIsRetry(response) {
|
81
82
|
return response.status >= 500;
|
82
83
|
}
|
84
|
+
const channels = {
|
85
|
+
request: node_diagnostics_channel_1.default.channel('urllib:request'),
|
86
|
+
response: node_diagnostics_channel_1.default.channel('urllib:response'),
|
87
|
+
};
|
83
88
|
class HttpClient extends node_events_1.EventEmitter {
|
84
89
|
#defaultArgs;
|
85
90
|
#dispatcher;
|
@@ -126,6 +131,7 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
126
131
|
const headers = {};
|
127
132
|
const args = {
|
128
133
|
retry: 0,
|
134
|
+
timing: true,
|
129
135
|
...this.#defaultArgs,
|
130
136
|
...options,
|
131
137
|
// keep method and headers exists on args for request event handler to easy use
|
@@ -136,7 +142,10 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
136
142
|
retries: 0,
|
137
143
|
...requestContext,
|
138
144
|
};
|
139
|
-
|
145
|
+
if (!requestContext.requestStartTime) {
|
146
|
+
requestContext.requestStartTime = node_perf_hooks_1.performance.now();
|
147
|
+
}
|
148
|
+
const requestStartTime = requestContext.requestStartTime;
|
140
149
|
// https://developer.chrome.com/docs/devtools/network/reference/?utm_source=devtools#timing-explanation
|
141
150
|
const timing = {
|
142
151
|
// socket assigned
|
@@ -384,6 +393,9 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
384
393
|
}
|
385
394
|
debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s', requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout);
|
386
395
|
requestOptions.headers = headers;
|
396
|
+
channels.request.publish({
|
397
|
+
request: reqMeta,
|
398
|
+
});
|
387
399
|
if (this.listenerCount('request') > 0) {
|
388
400
|
this.emit('request', reqMeta);
|
389
401
|
}
|
@@ -502,6 +514,10 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
502
514
|
return await this.#requestInternal(url, options, requestContext);
|
503
515
|
}
|
504
516
|
}
|
517
|
+
channels.response.publish({
|
518
|
+
request: reqMeta,
|
519
|
+
response: res,
|
520
|
+
});
|
505
521
|
if (this.listenerCount('response') > 0) {
|
506
522
|
this.emit('response', {
|
507
523
|
requestId,
|
@@ -525,16 +541,36 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
525
541
|
else if (err.name === 'BodyTimeoutError') {
|
526
542
|
err = new HttpClientRequestTimeoutError(bodyTimeout, { cause: e });
|
527
543
|
}
|
544
|
+
else if (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET') {
|
545
|
+
// auto retry on socket error, https://github.com/node-modules/urllib/issues/454
|
546
|
+
if (args.retry > 0 && requestContext.retries < args.retry) {
|
547
|
+
if (args.retryDelay) {
|
548
|
+
await (0, utils_1.sleep)(args.retryDelay);
|
549
|
+
}
|
550
|
+
requestContext.retries++;
|
551
|
+
return await this.#requestInternal(url, options, requestContext);
|
552
|
+
}
|
553
|
+
}
|
528
554
|
err.opaque = orginalOpaque;
|
529
555
|
err.status = res.status;
|
530
556
|
err.headers = res.headers;
|
531
557
|
err.res = res;
|
558
|
+
if (err.socket) {
|
559
|
+
// store rawSocket
|
560
|
+
err._rawSocket = err.socket;
|
561
|
+
}
|
562
|
+
err.socket = socketInfo;
|
532
563
|
// make sure requestUrls not empty
|
533
564
|
if (res.requestUrls.length === 0) {
|
534
565
|
res.requestUrls.push(requestUrl.href);
|
535
566
|
}
|
536
567
|
res.rt = (0, utils_1.performanceTime)(requestStartTime);
|
537
568
|
this.#updateSocketInfo(socketInfo, internalOpaque);
|
569
|
+
channels.response.publish({
|
570
|
+
request: reqMeta,
|
571
|
+
response: res,
|
572
|
+
error: err,
|
573
|
+
});
|
538
574
|
if (this.listenerCount('response') > 0) {
|
539
575
|
this.emit('response', {
|
540
576
|
requestId,
|
@@ -556,13 +592,16 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
556
592
|
socketInfo.id = socket[symbols_1.default.kSocketId];
|
557
593
|
socketInfo.handledRequests = socket[symbols_1.default.kHandledRequests];
|
558
594
|
socketInfo.handledResponses = socket[symbols_1.default.kHandledResponses];
|
559
|
-
socketInfo.localAddress = socket.
|
560
|
-
socketInfo.localPort = socket.
|
595
|
+
socketInfo.localAddress = socket[symbols_1.default.kSocketLocalAddress];
|
596
|
+
socketInfo.localPort = socket[symbols_1.default.kSocketLocalPort];
|
561
597
|
socketInfo.remoteAddress = socket.remoteAddress;
|
562
598
|
socketInfo.remotePort = socket.remotePort;
|
563
599
|
socketInfo.remoteFamily = socket.remoteFamily;
|
564
600
|
socketInfo.bytesRead = socket.bytesRead;
|
565
601
|
socketInfo.bytesWritten = socket.bytesWritten;
|
602
|
+
socketInfo.connectedTime = socket[symbols_1.default.kSocketConnectedTime];
|
603
|
+
socketInfo.lastRequestEndTime = socket[symbols_1.default.kSocketRequestEndTime];
|
604
|
+
socket[symbols_1.default.kSocketRequestEndTime] = new Date();
|
566
605
|
}
|
567
606
|
}
|
568
607
|
}
|
package/src/cjs/Request.d.ts
CHANGED
@@ -97,7 +97,7 @@ export type RequestOptions = {
|
|
97
97
|
* */
|
98
98
|
gzip?: boolean;
|
99
99
|
/**
|
100
|
-
* Enable timing or not, default is
|
100
|
+
* Enable timing or not, default is true.
|
101
101
|
* */
|
102
102
|
timing?: boolean;
|
103
103
|
/**
|
@@ -132,3 +132,10 @@ export type RequestOptions = {
|
|
132
132
|
/** Default: `64 KiB` */
|
133
133
|
highWaterMark?: number;
|
134
134
|
};
|
135
|
+
export type RequestMeta = {
|
136
|
+
requestId: number;
|
137
|
+
url: string;
|
138
|
+
args: RequestOptions;
|
139
|
+
ctx?: unknown;
|
140
|
+
retries: number;
|
141
|
+
};
|
package/src/cjs/Response.d.ts
CHANGED
@@ -21,6 +21,25 @@ let initedDiagnosticsChannel = false;
|
|
21
21
|
// server --> client
|
22
22
|
// undici:request:headers => { request, response }
|
23
23
|
// -> undici:request:trailers => { request, trailers }
|
24
|
+
function subscribe(name, listener) {
|
25
|
+
if (typeof node_diagnostics_channel_1.default.subscribe === 'function') {
|
26
|
+
node_diagnostics_channel_1.default.subscribe(name, listener);
|
27
|
+
}
|
28
|
+
else {
|
29
|
+
// TODO: support Node.js 14, will be removed on the next major version
|
30
|
+
node_diagnostics_channel_1.default.channel(name).subscribe(listener);
|
31
|
+
}
|
32
|
+
}
|
33
|
+
function formatSocket(socket) {
|
34
|
+
if (!socket)
|
35
|
+
return socket;
|
36
|
+
return {
|
37
|
+
localAddress: socket[symbols_1.default.kSocketLocalAddress],
|
38
|
+
localPort: socket[symbols_1.default.kSocketLocalPort],
|
39
|
+
remoteAddress: socket.remoteAddress,
|
40
|
+
remotePort: socket.remotePort,
|
41
|
+
};
|
42
|
+
}
|
24
43
|
function initDiagnosticsChannel() {
|
25
44
|
// makre sure init global DiagnosticsChannel once
|
26
45
|
if (initedDiagnosticsChannel)
|
@@ -29,7 +48,7 @@ function initDiagnosticsChannel() {
|
|
29
48
|
let kHandler;
|
30
49
|
// This message is published when a new outgoing request is created.
|
31
50
|
// Note: a request is only loosely completed to a given socket.
|
32
|
-
|
51
|
+
subscribe('undici:request:create', (message, name) => {
|
33
52
|
const { request } = message;
|
34
53
|
if (!kHandler) {
|
35
54
|
const symbols = Object.getOwnPropertySymbols(request);
|
@@ -52,16 +71,20 @@ function initDiagnosticsChannel() {
|
|
52
71
|
// diagnosticsChannel.channel('undici:client:beforeConnect')
|
53
72
|
// diagnosticsChannel.channel('undici:client:connectError')
|
54
73
|
// This message is published after a connection is established.
|
55
|
-
|
74
|
+
subscribe('undici:client:connected', (message, name) => {
|
56
75
|
const { socket } = message;
|
57
76
|
socket[symbols_1.default.kSocketId] = (0, utils_1.globalId)('UndiciSocket');
|
58
77
|
socket[symbols_1.default.kSocketStartTime] = node_perf_hooks_1.performance.now();
|
78
|
+
socket[symbols_1.default.kSocketConnectedTime] = new Date();
|
59
79
|
socket[symbols_1.default.kHandledRequests] = 0;
|
60
80
|
socket[symbols_1.default.kHandledResponses] = 0;
|
61
|
-
|
81
|
+
// copy local address to symbol, avoid them be reset after request error throw
|
82
|
+
socket[symbols_1.default.kSocketLocalAddress] = socket.localAddress;
|
83
|
+
socket[symbols_1.default.kSocketLocalPort] = socket.localPort;
|
84
|
+
debug('[%s] Socket#%d connected (sock: %o)', name, socket[symbols_1.default.kSocketId], formatSocket(socket));
|
62
85
|
});
|
63
86
|
// This message is published right before the first byte of the request is written to the socket.
|
64
|
-
|
87
|
+
subscribe('undici:client:sendHeaders', (message, name) => {
|
65
88
|
const { request, socket } = message;
|
66
89
|
if (!kHandler)
|
67
90
|
return;
|
@@ -71,7 +94,7 @@ function initDiagnosticsChannel() {
|
|
71
94
|
socket[symbols_1.default.kHandledRequests]++;
|
72
95
|
// attach socket to opaque
|
73
96
|
opaque[symbols_1.default.kRequestSocket] = socket;
|
74
|
-
debug('[%s] Request#%d send headers on Socket#%d (handled %d requests)', name, opaque[symbols_1.default.kRequestId], socket[symbols_1.default.kSocketId], socket[symbols_1.default.kHandledRequests]);
|
97
|
+
debug('[%s] Request#%d send headers on Socket#%d (handled %d requests, sock: %o)', name, opaque[symbols_1.default.kRequestId], socket[symbols_1.default.kSocketId], socket[symbols_1.default.kHandledRequests], formatSocket(socket));
|
75
98
|
if (!opaque[symbols_1.default.kEnableRequestTiming])
|
76
99
|
return;
|
77
100
|
opaque[symbols_1.default.kRequestTiming].requestHeadersSent = (0, utils_1.performanceTime)(opaque[symbols_1.default.kRequestStartTime]);
|
@@ -82,7 +105,7 @@ function initDiagnosticsChannel() {
|
|
82
105
|
(0, utils_1.performanceTime)(opaque[symbols_1.default.kRequestStartTime], socket[symbols_1.default.kSocketStartTime]);
|
83
106
|
}
|
84
107
|
});
|
85
|
-
|
108
|
+
subscribe('undici:request:bodySent', (message, name) => {
|
86
109
|
const { request } = message;
|
87
110
|
if (!kHandler)
|
88
111
|
return;
|
@@ -95,7 +118,7 @@ function initDiagnosticsChannel() {
|
|
95
118
|
opaque[symbols_1.default.kRequestTiming].requestSent = (0, utils_1.performanceTime)(opaque[symbols_1.default.kRequestStartTime]);
|
96
119
|
});
|
97
120
|
// This message is published after the response headers have been received, i.e. the response has been completed.
|
98
|
-
|
121
|
+
subscribe('undici:request:headers', (message, name) => {
|
99
122
|
const { request, response } = message;
|
100
123
|
if (!kHandler)
|
101
124
|
return;
|
@@ -105,13 +128,13 @@ function initDiagnosticsChannel() {
|
|
105
128
|
// get socket from opaque
|
106
129
|
const socket = opaque[symbols_1.default.kRequestSocket];
|
107
130
|
socket[symbols_1.default.kHandledResponses]++;
|
108
|
-
debug('[%s] Request#%d get %s response headers on Socket#%d (handled %d responses)', name, opaque[symbols_1.default.kRequestId], response.statusCode, socket[symbols_1.default.kSocketId], socket[symbols_1.default.kHandledResponses]);
|
131
|
+
debug('[%s] Request#%d get %s response headers on Socket#%d (handled %d responses, sock: %o)', name, opaque[symbols_1.default.kRequestId], response.statusCode, socket[symbols_1.default.kSocketId], socket[symbols_1.default.kHandledResponses], formatSocket(socket));
|
109
132
|
if (!opaque[symbols_1.default.kEnableRequestTiming])
|
110
133
|
return;
|
111
134
|
opaque[symbols_1.default.kRequestTiming].waiting = (0, utils_1.performanceTime)(opaque[symbols_1.default.kRequestStartTime]);
|
112
135
|
});
|
113
136
|
// This message is published after the response body and trailers have been received, i.e. the response has been completed.
|
114
|
-
|
137
|
+
subscribe('undici:request:trailers', (message, name) => {
|
115
138
|
const { request } = message;
|
116
139
|
if (!kHandler)
|
117
140
|
return;
|
@@ -123,6 +146,15 @@ function initDiagnosticsChannel() {
|
|
123
146
|
return;
|
124
147
|
opaque[symbols_1.default.kRequestTiming].contentDownload = (0, utils_1.performanceTime)(opaque[symbols_1.default.kRequestStartTime]);
|
125
148
|
});
|
126
|
-
//
|
149
|
+
// This message is published if the request is going to error, but it has not errored yet.
|
150
|
+
// subscribe('undici:request:error', (message, name) => {
|
151
|
+
// const { request, error } = message as DiagnosticsChannel.RequestErrorMessage;
|
152
|
+
// const opaque = request[kHandler]?.opts?.opaque;
|
153
|
+
// if (!opaque || !opaque[symbols.kRequestId]) return;
|
154
|
+
// const socket = opaque[symbols.kRequestSocket];
|
155
|
+
// debug('[%s] Request#%d error on Socket#%d (handled %d responses, sock: %o), error: %o',
|
156
|
+
// name, opaque[symbols.kRequestId], socket[symbols.kSocketId], socket[symbols.kHandledResponses],
|
157
|
+
// formatSocket(socket), error);
|
158
|
+
// });
|
127
159
|
}
|
128
160
|
exports.initDiagnosticsChannel = initDiagnosticsChannel;
|
package/src/cjs/index.d.ts
CHANGED
@@ -2,7 +2,7 @@ import { RequestOptions, RequestURL } from './Request';
|
|
2
2
|
export declare function request<T = any>(url: RequestURL, options?: RequestOptions): Promise<import("./Response").HttpClientResponse<T>>;
|
3
3
|
export declare function curl<T = any>(url: RequestURL, options?: RequestOptions): Promise<import("./Response").HttpClientResponse<T>>;
|
4
4
|
export { MockAgent, ProxyAgent, Agent, Dispatcher, setGlobalDispatcher, getGlobalDispatcher, } from 'undici';
|
5
|
-
export { HttpClient, HttpClient as HttpClient2, HEADER_USER_AGENT as USER_AGENT } from './HttpClient';
|
5
|
+
export { HttpClient, HttpClient as HttpClient2, HEADER_USER_AGENT as USER_AGENT, RequestDiagnosticsMessage, ResponseDiagnosticsMessage, } from './HttpClient';
|
6
6
|
export { RequestOptions, RequestOptions as RequestOptions2, RequestURL, HttpMethod, FixJSONCtlCharsHandler, FixJSONCtlChars, } from './Request';
|
7
7
|
export { SocketInfo, Timing, RawResponseWithMeta, HttpClientResponse } from './Response';
|
8
8
|
declare const _default: {
|
package/src/cjs/symbols.d.ts
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
declare const _default: {
|
2
2
|
kSocketId: symbol;
|
3
3
|
kSocketStartTime: symbol;
|
4
|
+
kSocketConnectedTime: symbol;
|
5
|
+
kSocketRequestEndTime: symbol;
|
6
|
+
kSocketLocalAddress: symbol;
|
7
|
+
kSocketLocalPort: symbol;
|
4
8
|
kHandledRequests: symbol;
|
5
9
|
kHandledResponses: symbol;
|
6
10
|
kRequestSocket: symbol;
|
package/src/cjs/symbols.js
CHANGED
@@ -3,6 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.default = {
|
4
4
|
kSocketId: Symbol('socket id'),
|
5
5
|
kSocketStartTime: Symbol('socket start time'),
|
6
|
+
kSocketConnectedTime: Symbol('socket connected time'),
|
7
|
+
kSocketRequestEndTime: Symbol('socket request end time'),
|
8
|
+
kSocketLocalAddress: Symbol('socket local address'),
|
9
|
+
kSocketLocalPort: Symbol('socket local port'),
|
6
10
|
kHandledRequests: Symbol('handled requests per socket'),
|
7
11
|
kHandledResponses: Symbol('handled responses per socket'),
|
8
12
|
kRequestSocket: Symbol('request on the socket'),
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import diagnosticsChannel from 'node:diagnostics_channel';
|
2
2
|
import { performance } from 'node:perf_hooks';
|
3
3
|
import { debuglog } from 'node:util';
|
4
|
+
import { Socket } from 'node:net';
|
4
5
|
import { DiagnosticsChannel } from 'undici';
|
5
6
|
import symbols from './symbols';
|
6
7
|
import { globalId, performanceTime } from './utils';
|
@@ -17,6 +18,26 @@ let initedDiagnosticsChannel = false;
|
|
17
18
|
// server --> client
|
18
19
|
// undici:request:headers => { request, response }
|
19
20
|
// -> undici:request:trailers => { request, trailers }
|
21
|
+
|
22
|
+
function subscribe(name: string, listener: (message: unknown, channelName: string | symbol) => void) {
|
23
|
+
if (typeof diagnosticsChannel.subscribe === 'function') {
|
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
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
function formatSocket(socket: Socket) {
|
32
|
+
if (!socket) return socket;
|
33
|
+
return {
|
34
|
+
localAddress: socket[symbols.kSocketLocalAddress],
|
35
|
+
localPort: socket[symbols.kSocketLocalPort],
|
36
|
+
remoteAddress: socket.remoteAddress,
|
37
|
+
remotePort: socket.remotePort,
|
38
|
+
};
|
39
|
+
}
|
40
|
+
|
20
41
|
export function initDiagnosticsChannel() {
|
21
42
|
// makre sure init global DiagnosticsChannel once
|
22
43
|
if (initedDiagnosticsChannel) return;
|
@@ -25,7 +46,7 @@ export function initDiagnosticsChannel() {
|
|
25
46
|
let kHandler: symbol;
|
26
47
|
// This message is published when a new outgoing request is created.
|
27
48
|
// Note: a request is only loosely completed to a given socket.
|
28
|
-
|
49
|
+
subscribe('undici:request:create', (message, name) => {
|
29
50
|
const { request } = message as DiagnosticsChannel.RequestCreateMessage;
|
30
51
|
if (!kHandler) {
|
31
52
|
const symbols = Object.getOwnPropertySymbols(request);
|
@@ -48,17 +69,21 @@ export function initDiagnosticsChannel() {
|
|
48
69
|
// diagnosticsChannel.channel('undici:client:beforeConnect')
|
49
70
|
// diagnosticsChannel.channel('undici:client:connectError')
|
50
71
|
// This message is published after a connection is established.
|
51
|
-
|
72
|
+
subscribe('undici:client:connected', (message, name) => {
|
52
73
|
const { socket } = message as DiagnosticsChannel.ClientConnectedMessage;
|
53
74
|
socket[symbols.kSocketId] = globalId('UndiciSocket');
|
54
75
|
socket[symbols.kSocketStartTime] = performance.now();
|
76
|
+
socket[symbols.kSocketConnectedTime] = new Date();
|
55
77
|
socket[symbols.kHandledRequests] = 0;
|
56
78
|
socket[symbols.kHandledResponses] = 0;
|
57
|
-
|
79
|
+
// copy local address to symbol, avoid them be reset after request error throw
|
80
|
+
socket[symbols.kSocketLocalAddress] = socket.localAddress;
|
81
|
+
socket[symbols.kSocketLocalPort] = socket.localPort;
|
82
|
+
debug('[%s] Socket#%d connected (sock: %o)', name, socket[symbols.kSocketId], formatSocket(socket));
|
58
83
|
});
|
59
84
|
|
60
85
|
// This message is published right before the first byte of the request is written to the socket.
|
61
|
-
|
86
|
+
subscribe('undici:client:sendHeaders', (message, name) => {
|
62
87
|
const { request, socket } = message as DiagnosticsChannel.ClientSendHeadersMessage;
|
63
88
|
if (!kHandler) return;
|
64
89
|
const opaque = request[kHandler]?.opts?.opaque;
|
@@ -67,8 +92,9 @@ export function initDiagnosticsChannel() {
|
|
67
92
|
socket[symbols.kHandledRequests]++;
|
68
93
|
// attach socket to opaque
|
69
94
|
opaque[symbols.kRequestSocket] = socket;
|
70
|
-
debug('[%s] Request#%d send headers on Socket#%d (handled %d requests)',
|
71
|
-
name, opaque[symbols.kRequestId], socket[symbols.kSocketId], socket[symbols.kHandledRequests]
|
95
|
+
debug('[%s] Request#%d send headers on Socket#%d (handled %d requests, sock: %o)',
|
96
|
+
name, opaque[symbols.kRequestId], socket[symbols.kSocketId], socket[symbols.kHandledRequests],
|
97
|
+
formatSocket(socket));
|
72
98
|
|
73
99
|
if (!opaque[symbols.kEnableRequestTiming]) return;
|
74
100
|
opaque[symbols.kRequestTiming].requestHeadersSent = performanceTime(opaque[symbols.kRequestStartTime]);
|
@@ -80,7 +106,7 @@ export function initDiagnosticsChannel() {
|
|
80
106
|
}
|
81
107
|
});
|
82
108
|
|
83
|
-
|
109
|
+
subscribe('undici:request:bodySent', (message, name) => {
|
84
110
|
const { request } = message as DiagnosticsChannel.RequestBodySentMessage;
|
85
111
|
if (!kHandler) return;
|
86
112
|
const opaque = request[kHandler]?.opts?.opaque;
|
@@ -92,7 +118,7 @@ export function initDiagnosticsChannel() {
|
|
92
118
|
});
|
93
119
|
|
94
120
|
// This message is published after the response headers have been received, i.e. the response has been completed.
|
95
|
-
|
121
|
+
subscribe('undici:request:headers', (message, name) => {
|
96
122
|
const { request, response } = message as DiagnosticsChannel.RequestHeadersMessage;
|
97
123
|
if (!kHandler) return;
|
98
124
|
const opaque = request[kHandler]?.opts?.opaque;
|
@@ -101,15 +127,16 @@ export function initDiagnosticsChannel() {
|
|
101
127
|
// get socket from opaque
|
102
128
|
const socket = opaque[symbols.kRequestSocket];
|
103
129
|
socket[symbols.kHandledResponses]++;
|
104
|
-
debug('[%s] Request#%d get %s response headers on Socket#%d (handled %d responses)',
|
105
|
-
name, opaque[symbols.kRequestId], response.statusCode, socket[symbols.kSocketId], socket[symbols.kHandledResponses]
|
130
|
+
debug('[%s] Request#%d get %s response headers on Socket#%d (handled %d responses, sock: %o)',
|
131
|
+
name, opaque[symbols.kRequestId], response.statusCode, socket[symbols.kSocketId], socket[symbols.kHandledResponses],
|
132
|
+
formatSocket(socket));
|
106
133
|
|
107
134
|
if (!opaque[symbols.kEnableRequestTiming]) return;
|
108
135
|
opaque[symbols.kRequestTiming].waiting = performanceTime(opaque[symbols.kRequestStartTime]);
|
109
136
|
});
|
110
137
|
|
111
138
|
// This message is published after the response body and trailers have been received, i.e. the response has been completed.
|
112
|
-
|
139
|
+
subscribe('undici:request:trailers', (message, name) => {
|
113
140
|
const { request } = message as DiagnosticsChannel.RequestTrailersMessage;
|
114
141
|
if (!kHandler) return;
|
115
142
|
const opaque = request[kHandler]?.opts?.opaque;
|
@@ -120,5 +147,15 @@ export function initDiagnosticsChannel() {
|
|
120
147
|
if (!opaque[symbols.kEnableRequestTiming]) return;
|
121
148
|
opaque[symbols.kRequestTiming].contentDownload = performanceTime(opaque[symbols.kRequestStartTime]);
|
122
149
|
});
|
123
|
-
|
150
|
+
|
151
|
+
// This message is published if the request is going to error, but it has not errored yet.
|
152
|
+
// subscribe('undici:request:error', (message, name) => {
|
153
|
+
// const { request, error } = message as DiagnosticsChannel.RequestErrorMessage;
|
154
|
+
// const opaque = request[kHandler]?.opts?.opaque;
|
155
|
+
// if (!opaque || !opaque[symbols.kRequestId]) return;
|
156
|
+
// const socket = opaque[symbols.kRequestSocket];
|
157
|
+
// debug('[%s] Request#%d error on Socket#%d (handled %d responses, sock: %o), error: %o',
|
158
|
+
// name, opaque[symbols.kRequestId], socket[symbols.kSocketId], socket[symbols.kHandledResponses],
|
159
|
+
// formatSocket(socket), error);
|
160
|
+
// });
|
124
161
|
}
|
package/src/esm/HttpClient.d.ts
CHANGED
@@ -4,8 +4,8 @@
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
5
5
|
import { LookupFunction } from 'node:net';
|
6
6
|
import { CheckAddressFunction } from './HttpAgent';
|
7
|
-
import { RequestURL, RequestOptions } from './Request';
|
8
|
-
import { HttpClientResponse } from './Response';
|
7
|
+
import { RequestURL, RequestOptions, RequestMeta } from './Request';
|
8
|
+
import { RawResponseWithMeta, HttpClientResponse } from './Response';
|
9
9
|
export type ClientOptions = {
|
10
10
|
defaultArgs?: RequestOptions;
|
11
11
|
/**
|
@@ -37,6 +37,14 @@ export type ClientOptions = {
|
|
37
37
|
};
|
38
38
|
};
|
39
39
|
export declare const HEADER_USER_AGENT: string;
|
40
|
+
export type RequestDiagnosticsMessage = {
|
41
|
+
request: RequestMeta;
|
42
|
+
};
|
43
|
+
export type ResponseDiagnosticsMessage = {
|
44
|
+
request: RequestMeta;
|
45
|
+
response: RawResponseWithMeta;
|
46
|
+
error?: Error;
|
47
|
+
};
|
40
48
|
export declare class HttpClient extends EventEmitter {
|
41
49
|
#private;
|
42
50
|
constructor(clientOptions?: ClientOptions);
|
package/src/esm/HttpClient.js
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import diagnosticsChannel from 'node:diagnostics_channel';
|
1
2
|
import { EventEmitter } from 'node:events';
|
2
3
|
import { STATUS_CODES } from 'node:http';
|
3
4
|
import { debuglog } from 'node:util';
|
@@ -63,7 +64,7 @@ class HttpClientRequestTimeoutError extends Error {
|
|
63
64
|
Error.captureStackTrace(this, this.constructor);
|
64
65
|
}
|
65
66
|
}
|
66
|
-
export const HEADER_USER_AGENT = createUserAgent('node-urllib', '3.
|
67
|
+
export const HEADER_USER_AGENT = createUserAgent('node-urllib', '3.17.0');
|
67
68
|
function getFileName(stream) {
|
68
69
|
const filePath = stream.path;
|
69
70
|
if (filePath) {
|
@@ -74,6 +75,10 @@ function getFileName(stream) {
|
|
74
75
|
function defaultIsRetry(response) {
|
75
76
|
return response.status >= 500;
|
76
77
|
}
|
78
|
+
const channels = {
|
79
|
+
request: diagnosticsChannel.channel('urllib:request'),
|
80
|
+
response: diagnosticsChannel.channel('urllib:response'),
|
81
|
+
};
|
77
82
|
export class HttpClient extends EventEmitter {
|
78
83
|
#defaultArgs;
|
79
84
|
#dispatcher;
|
@@ -120,6 +125,7 @@ export class HttpClient extends EventEmitter {
|
|
120
125
|
const headers = {};
|
121
126
|
const args = {
|
122
127
|
retry: 0,
|
128
|
+
timing: true,
|
123
129
|
...this.#defaultArgs,
|
124
130
|
...options,
|
125
131
|
// keep method and headers exists on args for request event handler to easy use
|
@@ -130,7 +136,10 @@ export class HttpClient extends EventEmitter {
|
|
130
136
|
retries: 0,
|
131
137
|
...requestContext,
|
132
138
|
};
|
133
|
-
|
139
|
+
if (!requestContext.requestStartTime) {
|
140
|
+
requestContext.requestStartTime = performance.now();
|
141
|
+
}
|
142
|
+
const requestStartTime = requestContext.requestStartTime;
|
134
143
|
// https://developer.chrome.com/docs/devtools/network/reference/?utm_source=devtools#timing-explanation
|
135
144
|
const timing = {
|
136
145
|
// socket assigned
|
@@ -378,6 +387,9 @@ export class HttpClient extends EventEmitter {
|
|
378
387
|
}
|
379
388
|
debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s', requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout);
|
380
389
|
requestOptions.headers = headers;
|
390
|
+
channels.request.publish({
|
391
|
+
request: reqMeta,
|
392
|
+
});
|
381
393
|
if (this.listenerCount('request') > 0) {
|
382
394
|
this.emit('request', reqMeta);
|
383
395
|
}
|
@@ -496,6 +508,10 @@ export class HttpClient extends EventEmitter {
|
|
496
508
|
return await this.#requestInternal(url, options, requestContext);
|
497
509
|
}
|
498
510
|
}
|
511
|
+
channels.response.publish({
|
512
|
+
request: reqMeta,
|
513
|
+
response: res,
|
514
|
+
});
|
499
515
|
if (this.listenerCount('response') > 0) {
|
500
516
|
this.emit('response', {
|
501
517
|
requestId,
|
@@ -519,16 +535,36 @@ export class HttpClient extends EventEmitter {
|
|
519
535
|
else if (err.name === 'BodyTimeoutError') {
|
520
536
|
err = new HttpClientRequestTimeoutError(bodyTimeout, { cause: e });
|
521
537
|
}
|
538
|
+
else if (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET') {
|
539
|
+
// auto retry on socket error, https://github.com/node-modules/urllib/issues/454
|
540
|
+
if (args.retry > 0 && requestContext.retries < args.retry) {
|
541
|
+
if (args.retryDelay) {
|
542
|
+
await sleep(args.retryDelay);
|
543
|
+
}
|
544
|
+
requestContext.retries++;
|
545
|
+
return await this.#requestInternal(url, options, requestContext);
|
546
|
+
}
|
547
|
+
}
|
522
548
|
err.opaque = orginalOpaque;
|
523
549
|
err.status = res.status;
|
524
550
|
err.headers = res.headers;
|
525
551
|
err.res = res;
|
552
|
+
if (err.socket) {
|
553
|
+
// store rawSocket
|
554
|
+
err._rawSocket = err.socket;
|
555
|
+
}
|
556
|
+
err.socket = socketInfo;
|
526
557
|
// make sure requestUrls not empty
|
527
558
|
if (res.requestUrls.length === 0) {
|
528
559
|
res.requestUrls.push(requestUrl.href);
|
529
560
|
}
|
530
561
|
res.rt = performanceTime(requestStartTime);
|
531
562
|
this.#updateSocketInfo(socketInfo, internalOpaque);
|
563
|
+
channels.response.publish({
|
564
|
+
request: reqMeta,
|
565
|
+
response: res,
|
566
|
+
error: err,
|
567
|
+
});
|
532
568
|
if (this.listenerCount('response') > 0) {
|
533
569
|
this.emit('response', {
|
534
570
|
requestId,
|
@@ -550,13 +586,16 @@ export class HttpClient extends EventEmitter {
|
|
550
586
|
socketInfo.id = socket[symbols.kSocketId];
|
551
587
|
socketInfo.handledRequests = socket[symbols.kHandledRequests];
|
552
588
|
socketInfo.handledResponses = socket[symbols.kHandledResponses];
|
553
|
-
socketInfo.localAddress = socket.
|
554
|
-
socketInfo.localPort = socket.
|
589
|
+
socketInfo.localAddress = socket[symbols.kSocketLocalAddress];
|
590
|
+
socketInfo.localPort = socket[symbols.kSocketLocalPort];
|
555
591
|
socketInfo.remoteAddress = socket.remoteAddress;
|
556
592
|
socketInfo.remotePort = socket.remotePort;
|
557
593
|
socketInfo.remoteFamily = socket.remoteFamily;
|
558
594
|
socketInfo.bytesRead = socket.bytesRead;
|
559
595
|
socketInfo.bytesWritten = socket.bytesWritten;
|
596
|
+
socketInfo.connectedTime = socket[symbols.kSocketConnectedTime];
|
597
|
+
socketInfo.lastRequestEndTime = socket[symbols.kSocketRequestEndTime];
|
598
|
+
socket[symbols.kSocketRequestEndTime] = new Date();
|
560
599
|
}
|
561
600
|
}
|
562
601
|
}
|
package/src/esm/Request.d.ts
CHANGED
@@ -97,7 +97,7 @@ export type RequestOptions = {
|
|
97
97
|
* */
|
98
98
|
gzip?: boolean;
|
99
99
|
/**
|
100
|
-
* Enable timing or not, default is
|
100
|
+
* Enable timing or not, default is true.
|
101
101
|
* */
|
102
102
|
timing?: boolean;
|
103
103
|
/**
|
@@ -132,3 +132,10 @@ export type RequestOptions = {
|
|
132
132
|
/** Default: `64 KiB` */
|
133
133
|
highWaterMark?: number;
|
134
134
|
};
|
135
|
+
export type RequestMeta = {
|
136
|
+
requestId: number;
|
137
|
+
url: string;
|
138
|
+
args: RequestOptions;
|
139
|
+
ctx?: unknown;
|
140
|
+
retries: number;
|
141
|
+
};
|
package/src/esm/Response.d.ts
CHANGED
@@ -15,6 +15,25 @@ let initedDiagnosticsChannel = false;
|
|
15
15
|
// server --> client
|
16
16
|
// undici:request:headers => { request, response }
|
17
17
|
// -> undici:request:trailers => { request, trailers }
|
18
|
+
function subscribe(name, listener) {
|
19
|
+
if (typeof diagnosticsChannel.subscribe === 'function') {
|
20
|
+
diagnosticsChannel.subscribe(name, listener);
|
21
|
+
}
|
22
|
+
else {
|
23
|
+
// TODO: support Node.js 14, will be removed on the next major version
|
24
|
+
diagnosticsChannel.channel(name).subscribe(listener);
|
25
|
+
}
|
26
|
+
}
|
27
|
+
function formatSocket(socket) {
|
28
|
+
if (!socket)
|
29
|
+
return socket;
|
30
|
+
return {
|
31
|
+
localAddress: socket[symbols.kSocketLocalAddress],
|
32
|
+
localPort: socket[symbols.kSocketLocalPort],
|
33
|
+
remoteAddress: socket.remoteAddress,
|
34
|
+
remotePort: socket.remotePort,
|
35
|
+
};
|
36
|
+
}
|
18
37
|
export function initDiagnosticsChannel() {
|
19
38
|
// makre sure init global DiagnosticsChannel once
|
20
39
|
if (initedDiagnosticsChannel)
|
@@ -23,7 +42,7 @@ export function initDiagnosticsChannel() {
|
|
23
42
|
let kHandler;
|
24
43
|
// This message is published when a new outgoing request is created.
|
25
44
|
// Note: a request is only loosely completed to a given socket.
|
26
|
-
|
45
|
+
subscribe('undici:request:create', (message, name) => {
|
27
46
|
const { request } = message;
|
28
47
|
if (!kHandler) {
|
29
48
|
const symbols = Object.getOwnPropertySymbols(request);
|
@@ -46,16 +65,20 @@ export function initDiagnosticsChannel() {
|
|
46
65
|
// diagnosticsChannel.channel('undici:client:beforeConnect')
|
47
66
|
// diagnosticsChannel.channel('undici:client:connectError')
|
48
67
|
// This message is published after a connection is established.
|
49
|
-
|
68
|
+
subscribe('undici:client:connected', (message, name) => {
|
50
69
|
const { socket } = message;
|
51
70
|
socket[symbols.kSocketId] = globalId('UndiciSocket');
|
52
71
|
socket[symbols.kSocketStartTime] = performance.now();
|
72
|
+
socket[symbols.kSocketConnectedTime] = new Date();
|
53
73
|
socket[symbols.kHandledRequests] = 0;
|
54
74
|
socket[symbols.kHandledResponses] = 0;
|
55
|
-
|
75
|
+
// copy local address to symbol, avoid them be reset after request error throw
|
76
|
+
socket[symbols.kSocketLocalAddress] = socket.localAddress;
|
77
|
+
socket[symbols.kSocketLocalPort] = socket.localPort;
|
78
|
+
debug('[%s] Socket#%d connected (sock: %o)', name, socket[symbols.kSocketId], formatSocket(socket));
|
56
79
|
});
|
57
80
|
// This message is published right before the first byte of the request is written to the socket.
|
58
|
-
|
81
|
+
subscribe('undici:client:sendHeaders', (message, name) => {
|
59
82
|
const { request, socket } = message;
|
60
83
|
if (!kHandler)
|
61
84
|
return;
|
@@ -65,7 +88,7 @@ export function initDiagnosticsChannel() {
|
|
65
88
|
socket[symbols.kHandledRequests]++;
|
66
89
|
// attach socket to opaque
|
67
90
|
opaque[symbols.kRequestSocket] = socket;
|
68
|
-
debug('[%s] Request#%d send headers on Socket#%d (handled %d requests)', name, opaque[symbols.kRequestId], socket[symbols.kSocketId], socket[symbols.kHandledRequests]);
|
91
|
+
debug('[%s] Request#%d send headers on Socket#%d (handled %d requests, sock: %o)', name, opaque[symbols.kRequestId], socket[symbols.kSocketId], socket[symbols.kHandledRequests], formatSocket(socket));
|
69
92
|
if (!opaque[symbols.kEnableRequestTiming])
|
70
93
|
return;
|
71
94
|
opaque[symbols.kRequestTiming].requestHeadersSent = performanceTime(opaque[symbols.kRequestStartTime]);
|
@@ -76,7 +99,7 @@ export function initDiagnosticsChannel() {
|
|
76
99
|
performanceTime(opaque[symbols.kRequestStartTime], socket[symbols.kSocketStartTime]);
|
77
100
|
}
|
78
101
|
});
|
79
|
-
|
102
|
+
subscribe('undici:request:bodySent', (message, name) => {
|
80
103
|
const { request } = message;
|
81
104
|
if (!kHandler)
|
82
105
|
return;
|
@@ -89,7 +112,7 @@ export function initDiagnosticsChannel() {
|
|
89
112
|
opaque[symbols.kRequestTiming].requestSent = performanceTime(opaque[symbols.kRequestStartTime]);
|
90
113
|
});
|
91
114
|
// This message is published after the response headers have been received, i.e. the response has been completed.
|
92
|
-
|
115
|
+
subscribe('undici:request:headers', (message, name) => {
|
93
116
|
const { request, response } = message;
|
94
117
|
if (!kHandler)
|
95
118
|
return;
|
@@ -99,13 +122,13 @@ export function initDiagnosticsChannel() {
|
|
99
122
|
// get socket from opaque
|
100
123
|
const socket = opaque[symbols.kRequestSocket];
|
101
124
|
socket[symbols.kHandledResponses]++;
|
102
|
-
debug('[%s] Request#%d get %s response headers on Socket#%d (handled %d responses)', name, opaque[symbols.kRequestId], response.statusCode, socket[symbols.kSocketId], socket[symbols.kHandledResponses]);
|
125
|
+
debug('[%s] Request#%d get %s response headers on Socket#%d (handled %d responses, sock: %o)', name, opaque[symbols.kRequestId], response.statusCode, socket[symbols.kSocketId], socket[symbols.kHandledResponses], formatSocket(socket));
|
103
126
|
if (!opaque[symbols.kEnableRequestTiming])
|
104
127
|
return;
|
105
128
|
opaque[symbols.kRequestTiming].waiting = performanceTime(opaque[symbols.kRequestStartTime]);
|
106
129
|
});
|
107
130
|
// This message is published after the response body and trailers have been received, i.e. the response has been completed.
|
108
|
-
|
131
|
+
subscribe('undici:request:trailers', (message, name) => {
|
109
132
|
const { request } = message;
|
110
133
|
if (!kHandler)
|
111
134
|
return;
|
@@ -117,5 +140,14 @@ export function initDiagnosticsChannel() {
|
|
117
140
|
return;
|
118
141
|
opaque[symbols.kRequestTiming].contentDownload = performanceTime(opaque[symbols.kRequestStartTime]);
|
119
142
|
});
|
120
|
-
//
|
143
|
+
// This message is published if the request is going to error, but it has not errored yet.
|
144
|
+
// subscribe('undici:request:error', (message, name) => {
|
145
|
+
// const { request, error } = message as DiagnosticsChannel.RequestErrorMessage;
|
146
|
+
// const opaque = request[kHandler]?.opts?.opaque;
|
147
|
+
// if (!opaque || !opaque[symbols.kRequestId]) return;
|
148
|
+
// const socket = opaque[symbols.kRequestSocket];
|
149
|
+
// debug('[%s] Request#%d error on Socket#%d (handled %d responses, sock: %o), error: %o',
|
150
|
+
// name, opaque[symbols.kRequestId], socket[symbols.kSocketId], socket[symbols.kHandledResponses],
|
151
|
+
// formatSocket(socket), error);
|
152
|
+
// });
|
121
153
|
}
|
package/src/esm/index.d.ts
CHANGED
@@ -2,7 +2,7 @@ import { RequestOptions, RequestURL } from './Request';
|
|
2
2
|
export declare function request<T = any>(url: RequestURL, options?: RequestOptions): Promise<import("./Response").HttpClientResponse<T>>;
|
3
3
|
export declare function curl<T = any>(url: RequestURL, options?: RequestOptions): Promise<import("./Response").HttpClientResponse<T>>;
|
4
4
|
export { MockAgent, ProxyAgent, Agent, Dispatcher, setGlobalDispatcher, getGlobalDispatcher, } from 'undici';
|
5
|
-
export { HttpClient, HttpClient as HttpClient2, HEADER_USER_AGENT as USER_AGENT } from './HttpClient';
|
5
|
+
export { HttpClient, HttpClient as HttpClient2, HEADER_USER_AGENT as USER_AGENT, RequestDiagnosticsMessage, ResponseDiagnosticsMessage, } from './HttpClient';
|
6
6
|
export { RequestOptions, RequestOptions as RequestOptions2, RequestURL, HttpMethod, FixJSONCtlCharsHandler, FixJSONCtlChars, } from './Request';
|
7
7
|
export { SocketInfo, Timing, RawResponseWithMeta, HttpClientResponse } from './Response';
|
8
8
|
declare const _default: {
|
package/src/esm/index.js
CHANGED
@@ -28,7 +28,7 @@ export async function curl(url, options) {
|
|
28
28
|
}
|
29
29
|
export { MockAgent, ProxyAgent, Agent, Dispatcher, setGlobalDispatcher, getGlobalDispatcher, } from 'undici';
|
30
30
|
// HttpClient2 is keep compatible with urlib@2 HttpClient2
|
31
|
-
export { HttpClient, HttpClient as HttpClient2, HEADER_USER_AGENT as USER_AGENT } from './HttpClient.js';
|
31
|
+
export { HttpClient, HttpClient as HttpClient2, HEADER_USER_AGENT as USER_AGENT, } from './HttpClient.js';
|
32
32
|
export default {
|
33
33
|
request,
|
34
34
|
curl,
|
package/src/esm/symbols.d.ts
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
declare const _default: {
|
2
2
|
kSocketId: symbol;
|
3
3
|
kSocketStartTime: symbol;
|
4
|
+
kSocketConnectedTime: symbol;
|
5
|
+
kSocketRequestEndTime: symbol;
|
6
|
+
kSocketLocalAddress: symbol;
|
7
|
+
kSocketLocalPort: symbol;
|
4
8
|
kHandledRequests: symbol;
|
5
9
|
kHandledResponses: symbol;
|
6
10
|
kRequestSocket: symbol;
|
package/src/esm/symbols.js
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
export default {
|
2
2
|
kSocketId: Symbol('socket id'),
|
3
3
|
kSocketStartTime: Symbol('socket start time'),
|
4
|
+
kSocketConnectedTime: Symbol('socket connected time'),
|
5
|
+
kSocketRequestEndTime: Symbol('socket request end time'),
|
6
|
+
kSocketLocalAddress: Symbol('socket local address'),
|
7
|
+
kSocketLocalPort: Symbol('socket local port'),
|
4
8
|
kHandledRequests: Symbol('handled requests per socket'),
|
5
9
|
kHandledResponses: Symbol('handled responses per socket'),
|
6
10
|
kRequestSocket: Symbol('request on the socket'),
|
package/src/index.ts
CHANGED
@@ -36,7 +36,10 @@ export {
|
|
36
36
|
setGlobalDispatcher, getGlobalDispatcher,
|
37
37
|
} from 'undici';
|
38
38
|
// HttpClient2 is keep compatible with urlib@2 HttpClient2
|
39
|
-
export {
|
39
|
+
export {
|
40
|
+
HttpClient, HttpClient as HttpClient2, HEADER_USER_AGENT as USER_AGENT,
|
41
|
+
RequestDiagnosticsMessage, ResponseDiagnosticsMessage,
|
42
|
+
} from './HttpClient';
|
40
43
|
// RequestOptions2 is keep compatible with urlib@2 RequestOptions2
|
41
44
|
export {
|
42
45
|
RequestOptions, RequestOptions as RequestOptions2, RequestURL, HttpMethod,
|
package/src/symbols.ts
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
export default {
|
2
2
|
kSocketId: Symbol('socket id'),
|
3
3
|
kSocketStartTime: Symbol('socket start time'),
|
4
|
+
kSocketConnectedTime: Symbol('socket connected time'),
|
5
|
+
kSocketRequestEndTime: Symbol('socket request end time'),
|
6
|
+
kSocketLocalAddress: Symbol('socket local address'),
|
7
|
+
kSocketLocalPort: Symbol('socket local port'),
|
4
8
|
kHandledRequests: Symbol('handled requests per socket'),
|
5
9
|
kHandledResponses: Symbol('handled responses per socket'),
|
6
10
|
kRequestSocket: Symbol('request on the socket'),
|