urllib 3.16.1 → 3.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,7 +64,8 @@ console.log('status: %s, body size: %d, headers: %j', res.status, data.length, r
64
64
  - ***dataType*** String - Type of response data. Could be `text` or `json`. If it's `text`, the `callback`ed `data` would be a String. If it's `json`, the `data` of callback would be a parsed JSON Object and will auto set `Accept: application/json` header. Default `callback`ed `data` would be a `Buffer`.
65
65
  - **fixJSONCtlChars** Boolean - Fix the control characters (U+0000 through U+001F) before JSON parse response. Default is `false`.
66
66
  - ***headers*** Object - Request headers.
67
- - ***timeout*** Number | Array - Request timeout in milliseconds for connecting phase and response receiving phase. Defaults to `exports.TIMEOUT`, both are 5s. You can use `timeout: 5000` to tell urllib use same timeout on two phase or set them seperately such as `timeout: [3000, 5000]`, which will set connecting timeout to 3s and response 5s.
67
+ - ***timeout*** Number | Array - Request timeout in milliseconds for connecting phase and response receiving phase. Default is `5000`. You can use `timeout: 5000` to tell urllib use same timeout on two phase or set them seperately such as `timeout: [3000, 5000]`, which will set connecting timeout to 3s and response 5s.
68
+ - **keepAliveTimeout** `number | null` - Default is `4000`, 4 seconds - The timeout after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server. See [MDN: HTTP - Headers - Keep-Alive directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive#directives) for more details.
68
69
  - ***auth*** String - `username:password` used in HTTP Basic Authorization.
69
70
  - ***digestAuth*** String - `username:password` used in HTTP [Digest Authorization](https://en.wikipedia.org/wiki/Digest_access_authentication).
70
71
  - ***followRedirect*** Boolean - follow HTTP 3xx responses as redirects. defaults to false.
@@ -72,8 +73,8 @@ console.log('status: %s, body size: %d, headers: %j', res.status, data.length, r
72
73
  - ***formatRedirectUrl*** Function - Format the redirect url by your self. Default is `url.resolve(from, to)`.
73
74
  - ***beforeRequest*** Function - Before request hook, you can change every thing here.
74
75
  - ***streaming*** Boolean - let you get the `res` object when request connected, default `false`. alias `customResponse`
75
- - ***compressed*** Boolean - Accept `gzip, br` response content and auto decode it, default is `false`.
76
- - ***timing*** Boolean - Enable timing or not, default is `false`.
76
+ - ***compressed*** Boolean - Accept `gzip, br` response content and auto decode it, default is `true`.
77
+ - ***timing*** Boolean - Enable timing or not, default is `true`.
77
78
  - ***socketPath*** String | null - request a unix socket service, default is `null`.
78
79
  - ***highWaterMark*** Number - default is `67108864`, 64 KiB.
79
80
 
@@ -200,10 +201,7 @@ Response is normal object, it contains:
200
201
  - `aborted`: response was aborted or not
201
202
  - `rt`: total request and response time in ms.
202
203
  - `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
204
+ - `socket`: socket info
207
205
 
208
206
  ## Run test with debug log
209
207
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "urllib",
3
- "version": "3.16.1",
3
+ "version": "3.17.1",
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 180000 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,26 @@ function defaultIsRetry(response: HttpClientResponse) {
137
138
 
138
139
  type RequestContext = {
139
140
  retries: number;
141
+ socketErrorRetries: number;
142
+ requestStartTime?: number;
140
143
  };
141
144
 
145
+ const channels = {
146
+ request: diagnosticsChannel.channel('urllib:request'),
147
+ response: diagnosticsChannel.channel('urllib:response'),
148
+ };
149
+
150
+ export type RequestDiagnosticsMessage = {
151
+ request: RequestMeta;
152
+ };
153
+
154
+ export type ResponseDiagnosticsMessage = {
155
+ request: RequestMeta;
156
+ response: RawResponseWithMeta;
157
+ error?: Error;
158
+ };
159
+
160
+
142
161
  export class HttpClient extends EventEmitter {
143
162
  #defaultArgs?: RequestOptions;
144
163
  #dispatcher?: Dispatcher;
@@ -188,6 +207,8 @@ export class HttpClient extends EventEmitter {
188
207
  const headers: IncomingHttpHeaders = {};
189
208
  const args = {
190
209
  retry: 0,
210
+ socketErrorRetry: 1,
211
+ timing: true,
191
212
  ...this.#defaultArgs,
192
213
  ...options,
193
214
  // keep method and headers exists on args for request event handler to easy use
@@ -196,9 +217,13 @@ export class HttpClient extends EventEmitter {
196
217
  };
197
218
  requestContext = {
198
219
  retries: 0,
220
+ socketErrorRetries: 0,
199
221
  ...requestContext,
200
222
  };
201
- const requestStartTime = performance.now();
223
+ if (!requestContext.requestStartTime) {
224
+ requestContext.requestStartTime = performance.now();
225
+ }
226
+ const requestStartTime = requestContext.requestStartTime;
202
227
 
203
228
  // https://developer.chrome.com/docs/devtools/network/reference/?utm_source=devtools#timing-explanation
204
229
  const timing = {
@@ -232,8 +257,8 @@ export class HttpClient extends EventEmitter {
232
257
  args,
233
258
  ctx: args.ctx,
234
259
  retries: requestContext.retries,
235
- };
236
- const socketInfo = {
260
+ } as RequestMeta;
261
+ const socketInfo: SocketInfo = {
237
262
  id: 0,
238
263
  localAddress: '',
239
264
  localPort: 0,
@@ -259,6 +284,8 @@ export class HttpClient extends EventEmitter {
259
284
  requestUrls: [],
260
285
  timing,
261
286
  socket: socketInfo,
287
+ retries: requestContext.retries,
288
+ socketErrorRetries: requestContext.socketErrorRetries,
262
289
  } as any as RawResponseWithMeta;
263
290
 
264
291
  let headersTimeout = 5000;
@@ -302,10 +329,19 @@ export class HttpClient extends EventEmitter {
302
329
  if (requestContext.retries > 0) {
303
330
  headers['x-urllib-retry'] = `${requestContext.retries}/${args.retry}`;
304
331
  }
332
+ if (requestContext.socketErrorRetries > 0) {
333
+ headers['x-urllib-retry-on-socket-error'] = `${requestContext.socketErrorRetries}/${args.socketErrorRetry}`;
334
+ }
305
335
  if (args.auth && !headers.authorization) {
306
336
  headers.authorization = `Basic ${Buffer.from(args.auth).toString('base64')}`;
307
337
  }
308
338
 
339
+ // streaming request should disable socketErrorRetry and retry
340
+ let isStreamingRequest = false;
341
+ if (args.dataType === 'stream' || args.writeStream) {
342
+ isStreamingRequest = true;
343
+ }
344
+
309
345
  try {
310
346
  const requestOptions: IUndiciRequestOption = {
311
347
  method,
@@ -334,9 +370,11 @@ export class HttpClient extends EventEmitter {
334
370
  if (isReadable(args.stream) && !(args.stream instanceof Readable)) {
335
371
  debug('Request#%d convert old style stream to Readable', requestId);
336
372
  args.stream = new Readable().wrap(args.stream);
373
+ isStreamingRequest = true;
337
374
  } else if (args.stream instanceof FormStream) {
338
375
  debug('Request#%d convert formstream to Readable', requestId);
339
376
  args.stream = new Readable().wrap(args.stream);
377
+ isStreamingRequest = true;
340
378
  }
341
379
  args.content = args.stream;
342
380
  }
@@ -380,6 +418,7 @@ export class HttpClient extends EventEmitter {
380
418
  } else if (file instanceof Readable || isReadable(file as any)) {
381
419
  const fileName = getFileName(file) || `streamfile${index}`;
382
420
  formData.append(field, new BlobFromStream(file, mime.lookup(fileName) || ''), fileName);
421
+ isStreamingRequest = true;
383
422
  }
384
423
  }
385
424
 
@@ -403,6 +442,7 @@ export class HttpClient extends EventEmitter {
403
442
  } else if (typeof args.content === 'string' && !headers['content-type']) {
404
443
  headers['content-type'] = 'text/plain;charset=UTF-8';
405
444
  }
445
+ isStreamingRequest = isReadable(args.content);
406
446
  }
407
447
  } else if (args.data) {
408
448
  const isStringOrBufferOrReadable = typeof args.data === 'string'
@@ -419,6 +459,7 @@ export class HttpClient extends EventEmitter {
419
459
  } else {
420
460
  if (isStringOrBufferOrReadable) {
421
461
  requestOptions.body = args.data;
462
+ isStreamingRequest = isReadable(args.data);
422
463
  } else {
423
464
  if (args.contentType === 'json'
424
465
  || args.contentType === 'application/json'
@@ -434,10 +475,17 @@ export class HttpClient extends EventEmitter {
434
475
  }
435
476
  }
436
477
  }
478
+ if (isStreamingRequest) {
479
+ args.retry = 0;
480
+ args.socketErrorRetry = 0;
481
+ }
437
482
 
438
- debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s',
439
- requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout);
483
+ debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s, isStreamingRequest: %s',
484
+ requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout, isStreamingRequest);
440
485
  requestOptions.headers = headers;
486
+ channels.request.publish({
487
+ request: reqMeta,
488
+ } as RequestDiagnosticsMessage);
441
489
  if (this.listenerCount('request') > 0) {
442
490
  this.emit('request', reqMeta);
443
491
  }
@@ -486,8 +534,6 @@ export class HttpClient extends EventEmitter {
486
534
 
487
535
  let data: any = null;
488
536
  if (args.dataType === 'stream') {
489
- // streaming mode will disable retry
490
- args.retry = 0;
491
537
  // only auto decompress on request args.compressed = true
492
538
  if (args.compressed === true && isCompressedContent) {
493
539
  // gzip or br
@@ -497,8 +543,6 @@ export class HttpClient extends EventEmitter {
497
543
  res = Object.assign(response.body, res);
498
544
  }
499
545
  } else if (args.writeStream) {
500
- // streaming mode will disable retry
501
- args.retry = 0;
502
546
  if (args.compressed === true && isCompressedContent) {
503
547
  const decoder = contentEncoding === 'gzip' ? createGunzip() : createBrotliDecompress();
504
548
  await pipelinePromise(response.body, decoder, args.writeStream);
@@ -556,6 +600,10 @@ export class HttpClient extends EventEmitter {
556
600
  }
557
601
  }
558
602
 
603
+ channels.response.publish({
604
+ request: reqMeta,
605
+ response: res,
606
+ } as ResponseDiagnosticsMessage);
559
607
  if (this.listenerCount('response') > 0) {
560
608
  this.emit('response', {
561
609
  requestId,
@@ -577,11 +625,22 @@ export class HttpClient extends EventEmitter {
577
625
  err = new HttpClientRequestTimeoutError(headersTimeout, { cause: e });
578
626
  } else if (err.name === 'BodyTimeoutError') {
579
627
  err = new HttpClientRequestTimeoutError(bodyTimeout, { cause: e });
628
+ } else if (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET') {
629
+ // auto retry on socket error, https://github.com/node-modules/urllib/issues/454
630
+ if (args.socketErrorRetry > 0 && requestContext.socketErrorRetries < args.socketErrorRetry) {
631
+ requestContext.socketErrorRetries++;
632
+ return await this.#requestInternal(url, options, requestContext);
633
+ }
580
634
  }
581
635
  err.opaque = orginalOpaque;
582
636
  err.status = res.status;
583
637
  err.headers = res.headers;
584
638
  err.res = res;
639
+ if (err.socket) {
640
+ // store rawSocket
641
+ err._rawSocket = err.socket;
642
+ }
643
+ err.socket = socketInfo;
585
644
  // make sure requestUrls not empty
586
645
  if (res.requestUrls.length === 0) {
587
646
  res.requestUrls.push(requestUrl.href);
@@ -589,6 +648,11 @@ export class HttpClient extends EventEmitter {
589
648
  res.rt = performanceTime(requestStartTime);
590
649
  this.#updateSocketInfo(socketInfo, internalOpaque);
591
650
 
651
+ channels.response.publish({
652
+ request: reqMeta,
653
+ response: res,
654
+ error: err,
655
+ } as ResponseDiagnosticsMessage);
592
656
  if (this.listenerCount('response') > 0) {
593
657
  this.emit('response', {
594
658
  requestId,
@@ -611,13 +675,16 @@ export class HttpClient extends EventEmitter {
611
675
  socketInfo.id = socket[symbols.kSocketId];
612
676
  socketInfo.handledRequests = socket[symbols.kHandledRequests];
613
677
  socketInfo.handledResponses = socket[symbols.kHandledResponses];
614
- socketInfo.localAddress = socket.localAddress;
615
- socketInfo.localPort = socket.localPort;
678
+ socketInfo.localAddress = socket[symbols.kSocketLocalAddress];
679
+ socketInfo.localPort = socket[symbols.kSocketLocalPort];
616
680
  socketInfo.remoteAddress = socket.remoteAddress;
617
681
  socketInfo.remotePort = socket.remotePort;
618
682
  socketInfo.remoteFamily = socket.remoteFamily;
619
683
  socketInfo.bytesRead = socket.bytesRead;
620
684
  socketInfo.bytesWritten = socket.bytesWritten;
685
+ socketInfo.connectedTime = socket[symbols.kSocketConnectedTime];
686
+ socketInfo.lastRequestEndTime = socket[symbols.kSocketRequestEndTime];
687
+ socket[symbols.kSocketRequestEndTime] = new Date();
621
688
  }
622
689
  }
623
690
  }
package/src/Request.ts CHANGED
@@ -69,11 +69,16 @@ export type RequestOptions = {
69
69
  headers?: IncomingHttpHeaders;
70
70
  /**
71
71
  * Request timeout in milliseconds for connecting phase and response receiving phase.
72
- * Defaults to exports.
73
- * TIMEOUT, both are 5s. You can use timeout: 5000 to tell urllib use same timeout on two phase or set them seperately such as
72
+ * Defaults is `5000`, both are 5 seconds. You can use timeout: 5000 to tell urllib use same timeout on two phase or set them separately such as
74
73
  * timeout: [3000, 5000], which will set connecting timeout to 3s and response 5s.
75
74
  */
76
75
  timeout?: number | number[];
76
+ /**
77
+ * Default is `4000`, 4 seconds - The timeout after which a socket without active requests will time out.
78
+ * Monitors time between activity on a connected socket.
79
+ * This value may be overridden by *keep-alive* hints from the server. See [MDN: HTTP - Headers - Keep-Alive directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive#directives) for more details.
80
+ */
81
+ keepAliveTimeout?: number;
77
82
  /**
78
83
  * username:password used in HTTP Basic Authorization.
79
84
  * Alias to `headers.authorization = xxx`
@@ -91,7 +96,7 @@ export type RequestOptions = {
91
96
  formatRedirectUrl?: (a: any, b: any) => void;
92
97
  /** Before request hook, you can change every thing here. */
93
98
  beforeRequest?: (...args: any[]) => void;
94
- /** Accept `gzip, br` response content and auto decode it, default is false. */
99
+ /** Accept `gzip, br` response content and auto decode it, default is `true`. */
95
100
  compressed?: boolean;
96
101
  /**
97
102
  * @deprecated
@@ -99,11 +104,11 @@ export type RequestOptions = {
99
104
  * */
100
105
  gzip?: boolean;
101
106
  /**
102
- * Enable timing or not, default is false.
107
+ * Enable timing or not, default is `true`.
103
108
  * */
104
109
  timing?: boolean;
105
110
  /**
106
- * Auto retry times on 5xx response, default is 0. Don't work on streaming request
111
+ * Auto retry times on 5xx response, default is `0`. Don't work on streaming request
107
112
  * It's not supported by using retry and writeStream, because the retry request can't stop the stream which is consuming.
108
113
  **/
109
114
  retry?: number;
@@ -114,6 +119,11 @@ export type RequestOptions = {
114
119
  * It will retry when status >= 500 by default. Request error is not included.
115
120
  */
116
121
  isRetry?: (response: HttpClientResponse) => boolean;
122
+ /**
123
+ * Auto retry times on socket error, default is `1`. Don't work on streaming request
124
+ * It's not supported by using retry and writeStream, because the retry request can't stop the stream which is consuming.
125
+ **/
126
+ socketErrorRetry?: number;
117
127
  /** Default: `null` */
118
128
  opaque?: unknown;
119
129
  /**
@@ -134,3 +144,11 @@ export type RequestOptions = {
134
144
  /** Default: `64 KiB` */
135
145
  highWaterMark?: number;
136
146
  };
147
+
148
+ export type RequestMeta = {
149
+ requestId: number;
150
+ url: string;
151
+ args: RequestOptions;
152
+ ctx?: unknown;
153
+ retries: number;
154
+ };
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
  /**
@@ -47,6 +49,8 @@ export type RawResponseWithMeta = Readable & {
47
49
  rt: number;
48
50
  keepAliveSocket: boolean;
49
51
  requestUrls: string[];
52
+ retries: number;
53
+ socketErrorRetries: number;
50
54
  };
51
55
 
52
56
  export type HttpClientResponse<T = any> = {
@@ -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.1');
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,8 @@ class HttpClient extends node_events_1.EventEmitter {
126
131
  const headers = {};
127
132
  const args = {
128
133
  retry: 0,
134
+ socketErrorRetry: 1,
135
+ timing: true,
129
136
  ...this.#defaultArgs,
130
137
  ...options,
131
138
  // keep method and headers exists on args for request event handler to easy use
@@ -134,9 +141,13 @@ class HttpClient extends node_events_1.EventEmitter {
134
141
  };
135
142
  requestContext = {
136
143
  retries: 0,
144
+ socketErrorRetries: 0,
137
145
  ...requestContext,
138
146
  };
139
- const requestStartTime = node_perf_hooks_1.performance.now();
147
+ if (!requestContext.requestStartTime) {
148
+ requestContext.requestStartTime = node_perf_hooks_1.performance.now();
149
+ }
150
+ const requestStartTime = requestContext.requestStartTime;
140
151
  // https://developer.chrome.com/docs/devtools/network/reference/?utm_source=devtools#timing-explanation
141
152
  const timing = {
142
153
  // socket assigned
@@ -196,6 +207,8 @@ class HttpClient extends node_events_1.EventEmitter {
196
207
  requestUrls: [],
197
208
  timing,
198
209
  socket: socketInfo,
210
+ retries: requestContext.retries,
211
+ socketErrorRetries: requestContext.socketErrorRetries,
199
212
  };
200
213
  let headersTimeout = 5000;
201
214
  let bodyTimeout = 5000;
@@ -240,9 +253,17 @@ class HttpClient extends node_events_1.EventEmitter {
240
253
  if (requestContext.retries > 0) {
241
254
  headers['x-urllib-retry'] = `${requestContext.retries}/${args.retry}`;
242
255
  }
256
+ if (requestContext.socketErrorRetries > 0) {
257
+ headers['x-urllib-retry-on-socket-error'] = `${requestContext.socketErrorRetries}/${args.socketErrorRetry}`;
258
+ }
243
259
  if (args.auth && !headers.authorization) {
244
260
  headers.authorization = `Basic ${Buffer.from(args.auth).toString('base64')}`;
245
261
  }
262
+ // streaming request should disable socketErrorRetry and retry
263
+ let isStreamingRequest = false;
264
+ if (args.dataType === 'stream' || args.writeStream) {
265
+ isStreamingRequest = true;
266
+ }
246
267
  try {
247
268
  const requestOptions = {
248
269
  method,
@@ -270,10 +291,12 @@ class HttpClient extends node_events_1.EventEmitter {
270
291
  if ((0, utils_1.isReadable)(args.stream) && !(args.stream instanceof node_stream_1.Readable)) {
271
292
  debug('Request#%d convert old style stream to Readable', requestId);
272
293
  args.stream = new node_stream_1.Readable().wrap(args.stream);
294
+ isStreamingRequest = true;
273
295
  }
274
296
  else if (args.stream instanceof formstream_1.default) {
275
297
  debug('Request#%d convert formstream to Readable', requestId);
276
298
  args.stream = new node_stream_1.Readable().wrap(args.stream);
299
+ isStreamingRequest = true;
277
300
  }
278
301
  args.content = args.stream;
279
302
  }
@@ -321,6 +344,7 @@ class HttpClient extends node_events_1.EventEmitter {
321
344
  else if (file instanceof node_stream_1.Readable || (0, utils_1.isReadable)(file)) {
322
345
  const fileName = getFileName(file) || `streamfile${index}`;
323
346
  formData.append(field, new BlobFromStream(file, mime_types_1.default.lookup(fileName) || ''), fileName);
347
+ isStreamingRequest = true;
324
348
  }
325
349
  }
326
350
  if (undici_1.FormData) {
@@ -346,6 +370,7 @@ class HttpClient extends node_events_1.EventEmitter {
346
370
  else if (typeof args.content === 'string' && !headers['content-type']) {
347
371
  headers['content-type'] = 'text/plain;charset=UTF-8';
348
372
  }
373
+ isStreamingRequest = (0, utils_1.isReadable)(args.content);
349
374
  }
350
375
  }
351
376
  else if (args.data) {
@@ -365,6 +390,7 @@ class HttpClient extends node_events_1.EventEmitter {
365
390
  else {
366
391
  if (isStringOrBufferOrReadable) {
367
392
  requestOptions.body = args.data;
393
+ isStreamingRequest = (0, utils_1.isReadable)(args.data);
368
394
  }
369
395
  else {
370
396
  if (args.contentType === 'json'
@@ -382,8 +408,15 @@ class HttpClient extends node_events_1.EventEmitter {
382
408
  }
383
409
  }
384
410
  }
385
- debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s', requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout);
411
+ if (isStreamingRequest) {
412
+ args.retry = 0;
413
+ args.socketErrorRetry = 0;
414
+ }
415
+ debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s, isStreamingRequest: %s', requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout, isStreamingRequest);
386
416
  requestOptions.headers = headers;
417
+ channels.request.publish({
418
+ request: reqMeta,
419
+ });
387
420
  if (this.listenerCount('request') > 0) {
388
421
  this.emit('request', reqMeta);
389
422
  }
@@ -428,8 +461,6 @@ class HttpClient extends node_events_1.EventEmitter {
428
461
  }
429
462
  let data = null;
430
463
  if (args.dataType === 'stream') {
431
- // streaming mode will disable retry
432
- args.retry = 0;
433
464
  // only auto decompress on request args.compressed = true
434
465
  if (args.compressed === true && isCompressedContent) {
435
466
  // gzip or br
@@ -441,8 +472,6 @@ class HttpClient extends node_events_1.EventEmitter {
441
472
  }
442
473
  }
443
474
  else if (args.writeStream) {
444
- // streaming mode will disable retry
445
- args.retry = 0;
446
475
  if (args.compressed === true && isCompressedContent) {
447
476
  const decoder = contentEncoding === 'gzip' ? (0, node_zlib_1.createGunzip)() : (0, node_zlib_1.createBrotliDecompress)();
448
477
  await pipelinePromise(response.body, decoder, args.writeStream);
@@ -502,6 +531,10 @@ class HttpClient extends node_events_1.EventEmitter {
502
531
  return await this.#requestInternal(url, options, requestContext);
503
532
  }
504
533
  }
534
+ channels.response.publish({
535
+ request: reqMeta,
536
+ response: res,
537
+ });
505
538
  if (this.listenerCount('response') > 0) {
506
539
  this.emit('response', {
507
540
  requestId,
@@ -525,16 +558,33 @@ class HttpClient extends node_events_1.EventEmitter {
525
558
  else if (err.name === 'BodyTimeoutError') {
526
559
  err = new HttpClientRequestTimeoutError(bodyTimeout, { cause: e });
527
560
  }
561
+ else if (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET') {
562
+ // auto retry on socket error, https://github.com/node-modules/urllib/issues/454
563
+ if (args.socketErrorRetry > 0 && requestContext.socketErrorRetries < args.socketErrorRetry) {
564
+ requestContext.socketErrorRetries++;
565
+ return await this.#requestInternal(url, options, requestContext);
566
+ }
567
+ }
528
568
  err.opaque = orginalOpaque;
529
569
  err.status = res.status;
530
570
  err.headers = res.headers;
531
571
  err.res = res;
572
+ if (err.socket) {
573
+ // store rawSocket
574
+ err._rawSocket = err.socket;
575
+ }
576
+ err.socket = socketInfo;
532
577
  // make sure requestUrls not empty
533
578
  if (res.requestUrls.length === 0) {
534
579
  res.requestUrls.push(requestUrl.href);
535
580
  }
536
581
  res.rt = (0, utils_1.performanceTime)(requestStartTime);
537
582
  this.#updateSocketInfo(socketInfo, internalOpaque);
583
+ channels.response.publish({
584
+ request: reqMeta,
585
+ response: res,
586
+ error: err,
587
+ });
538
588
  if (this.listenerCount('response') > 0) {
539
589
  this.emit('response', {
540
590
  requestId,
@@ -556,13 +606,16 @@ class HttpClient extends node_events_1.EventEmitter {
556
606
  socketInfo.id = socket[symbols_1.default.kSocketId];
557
607
  socketInfo.handledRequests = socket[symbols_1.default.kHandledRequests];
558
608
  socketInfo.handledResponses = socket[symbols_1.default.kHandledResponses];
559
- socketInfo.localAddress = socket.localAddress;
560
- socketInfo.localPort = socket.localPort;
609
+ socketInfo.localAddress = socket[symbols_1.default.kSocketLocalAddress];
610
+ socketInfo.localPort = socket[symbols_1.default.kSocketLocalPort];
561
611
  socketInfo.remoteAddress = socket.remoteAddress;
562
612
  socketInfo.remotePort = socket.remotePort;
563
613
  socketInfo.remoteFamily = socket.remoteFamily;
564
614
  socketInfo.bytesRead = socket.bytesRead;
565
615
  socketInfo.bytesWritten = socket.bytesWritten;
616
+ socketInfo.connectedTime = socket[symbols_1.default.kSocketConnectedTime];
617
+ socketInfo.lastRequestEndTime = socket[symbols_1.default.kSocketRequestEndTime];
618
+ socket[symbols_1.default.kSocketRequestEndTime] = new Date();
566
619
  }
567
620
  }
568
621
  }