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 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
- - `remoteAddress`: http server ip address
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.16.1",
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-c8": "^0.29.7",
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.31.1"
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
- const requestStartTime = performance.now();
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.localAddress;
615
- socketInfo.localPort = socket.localPort;
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 false.
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
@@ -12,6 +12,8 @@ export type SocketInfo = {
12
12
  bytesRead: number;
13
13
  handledRequests: number;
14
14
  handledResponses: number;
15
+ connectedTime?: Date;
16
+ lastRequestEndTime?: Date;
15
17
  };
16
18
 
17
19
  /**
@@ -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);
@@ -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.16.1');
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
- const requestStartTime = node_perf_hooks_1.performance.now();
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.localAddress;
560
- socketInfo.localPort = socket.localPort;
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
  }
@@ -97,7 +97,7 @@ export type RequestOptions = {
97
97
  * */
98
98
  gzip?: boolean;
99
99
  /**
100
- * Enable timing or not, default is false.
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
+ };
@@ -13,6 +13,8 @@ export type SocketInfo = {
13
13
  bytesRead: number;
14
14
  handledRequests: number;
15
15
  handledResponses: number;
16
+ connectedTime?: Date;
17
+ lastRequestEndTime?: Date;
16
18
  };
17
19
  /**
18
20
  * https://eggjs.org/en/core/httpclient.html#timing-boolean
@@ -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
- node_diagnostics_channel_1.default.channel('undici:request:create').subscribe((message, name) => {
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
- node_diagnostics_channel_1.default.channel('undici:client:connected').subscribe((message, name) => {
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
- debug('[%s] Socket#%d connected', name, socket[symbols_1.default.kSocketId]);
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
- node_diagnostics_channel_1.default.channel('undici:client:sendHeaders').subscribe((message, name) => {
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
- node_diagnostics_channel_1.default.channel('undici:request:bodySent').subscribe((message, name) => {
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
- node_diagnostics_channel_1.default.channel('undici:request:headers').subscribe((message, name) => {
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
- node_diagnostics_channel_1.default.channel('undici:request:trailers').subscribe((message, name) => {
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
- // diagnosticsChannel.channel('undici:request:error')
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;
@@ -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: {
@@ -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;
@@ -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
- diagnosticsChannel.channel('undici:request:create').subscribe((message, name) => {
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
- diagnosticsChannel.channel('undici:client:connected').subscribe((message, name) => {
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
- debug('[%s] Socket#%d connected', name, socket[symbols.kSocketId]);
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
- diagnosticsChannel.channel('undici:client:sendHeaders').subscribe((message, name) => {
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
- diagnosticsChannel.channel('undici:request:bodySent').subscribe((message, name) => {
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
- diagnosticsChannel.channel('undici:request:headers').subscribe((message, name) => {
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
- diagnosticsChannel.channel('undici:request:trailers').subscribe((message, name) => {
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
- // diagnosticsChannel.channel('undici:request:error')
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
  }
@@ -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);
@@ -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.16.1');
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
- const requestStartTime = performance.now();
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.localAddress;
554
- socketInfo.localPort = socket.localPort;
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
  }
@@ -97,7 +97,7 @@ export type RequestOptions = {
97
97
  * */
98
98
  gzip?: boolean;
99
99
  /**
100
- * Enable timing or not, default is false.
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
+ };
@@ -13,6 +13,8 @@ export type SocketInfo = {
13
13
  bytesRead: number;
14
14
  handledRequests: number;
15
15
  handledResponses: number;
16
+ connectedTime?: Date;
17
+ lastRequestEndTime?: Date;
16
18
  };
17
19
  /**
18
20
  * https://eggjs.org/en/core/httpclient.html#timing-boolean
@@ -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
- diagnosticsChannel.channel('undici:request:create').subscribe((message, name) => {
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
- diagnosticsChannel.channel('undici:client:connected').subscribe((message, name) => {
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
- debug('[%s] Socket#%d connected', name, socket[symbols.kSocketId]);
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
- diagnosticsChannel.channel('undici:client:sendHeaders').subscribe((message, name) => {
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
- diagnosticsChannel.channel('undici:request:bodySent').subscribe((message, name) => {
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
- diagnosticsChannel.channel('undici:request:headers').subscribe((message, name) => {
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
- diagnosticsChannel.channel('undici:request:trailers').subscribe((message, name) => {
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
- // diagnosticsChannel.channel('undici:request:error')
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
  }
@@ -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,
@@ -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;
@@ -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 { HttpClient, HttpClient as HttpClient2, HEADER_USER_AGENT as USER_AGENT } from './HttpClient';
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'),