urllib 3.17.0 → 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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "urllib",
3
- "version": "3.17.0",
3
+ "version": "3.17.1",
4
4
  "publishConfig": {
5
5
  "tag": "latest"
6
6
  },
@@ -53,7 +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
+ "test-keepalive": "cross-env TEST_KEEPALIVE_COUNT=50 vitest run --test-timeout 180000 keep-alive-header.test.ts",
57
57
  "cov": "vitest run --coverage",
58
58
  "ci": "npm run lint && npm run cov && npm run build:test",
59
59
  "contributor": "git-contributor",
package/src/HttpClient.ts CHANGED
@@ -138,6 +138,7 @@ function defaultIsRetry(response: HttpClientResponse) {
138
138
 
139
139
  type RequestContext = {
140
140
  retries: number;
141
+ socketErrorRetries: number;
141
142
  requestStartTime?: number;
142
143
  };
143
144
 
@@ -206,6 +207,7 @@ export class HttpClient extends EventEmitter {
206
207
  const headers: IncomingHttpHeaders = {};
207
208
  const args = {
208
209
  retry: 0,
210
+ socketErrorRetry: 1,
209
211
  timing: true,
210
212
  ...this.#defaultArgs,
211
213
  ...options,
@@ -215,6 +217,7 @@ export class HttpClient extends EventEmitter {
215
217
  };
216
218
  requestContext = {
217
219
  retries: 0,
220
+ socketErrorRetries: 0,
218
221
  ...requestContext,
219
222
  };
220
223
  if (!requestContext.requestStartTime) {
@@ -281,6 +284,8 @@ export class HttpClient extends EventEmitter {
281
284
  requestUrls: [],
282
285
  timing,
283
286
  socket: socketInfo,
287
+ retries: requestContext.retries,
288
+ socketErrorRetries: requestContext.socketErrorRetries,
284
289
  } as any as RawResponseWithMeta;
285
290
 
286
291
  let headersTimeout = 5000;
@@ -324,10 +329,19 @@ export class HttpClient extends EventEmitter {
324
329
  if (requestContext.retries > 0) {
325
330
  headers['x-urllib-retry'] = `${requestContext.retries}/${args.retry}`;
326
331
  }
332
+ if (requestContext.socketErrorRetries > 0) {
333
+ headers['x-urllib-retry-on-socket-error'] = `${requestContext.socketErrorRetries}/${args.socketErrorRetry}`;
334
+ }
327
335
  if (args.auth && !headers.authorization) {
328
336
  headers.authorization = `Basic ${Buffer.from(args.auth).toString('base64')}`;
329
337
  }
330
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
+
331
345
  try {
332
346
  const requestOptions: IUndiciRequestOption = {
333
347
  method,
@@ -356,9 +370,11 @@ export class HttpClient extends EventEmitter {
356
370
  if (isReadable(args.stream) && !(args.stream instanceof Readable)) {
357
371
  debug('Request#%d convert old style stream to Readable', requestId);
358
372
  args.stream = new Readable().wrap(args.stream);
373
+ isStreamingRequest = true;
359
374
  } else if (args.stream instanceof FormStream) {
360
375
  debug('Request#%d convert formstream to Readable', requestId);
361
376
  args.stream = new Readable().wrap(args.stream);
377
+ isStreamingRequest = true;
362
378
  }
363
379
  args.content = args.stream;
364
380
  }
@@ -402,6 +418,7 @@ export class HttpClient extends EventEmitter {
402
418
  } else if (file instanceof Readable || isReadable(file as any)) {
403
419
  const fileName = getFileName(file) || `streamfile${index}`;
404
420
  formData.append(field, new BlobFromStream(file, mime.lookup(fileName) || ''), fileName);
421
+ isStreamingRequest = true;
405
422
  }
406
423
  }
407
424
 
@@ -425,6 +442,7 @@ export class HttpClient extends EventEmitter {
425
442
  } else if (typeof args.content === 'string' && !headers['content-type']) {
426
443
  headers['content-type'] = 'text/plain;charset=UTF-8';
427
444
  }
445
+ isStreamingRequest = isReadable(args.content);
428
446
  }
429
447
  } else if (args.data) {
430
448
  const isStringOrBufferOrReadable = typeof args.data === 'string'
@@ -441,6 +459,7 @@ export class HttpClient extends EventEmitter {
441
459
  } else {
442
460
  if (isStringOrBufferOrReadable) {
443
461
  requestOptions.body = args.data;
462
+ isStreamingRequest = isReadable(args.data);
444
463
  } else {
445
464
  if (args.contentType === 'json'
446
465
  || args.contentType === 'application/json'
@@ -456,9 +475,13 @@ export class HttpClient extends EventEmitter {
456
475
  }
457
476
  }
458
477
  }
478
+ if (isStreamingRequest) {
479
+ args.retry = 0;
480
+ args.socketErrorRetry = 0;
481
+ }
459
482
 
460
- debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s',
461
- 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);
462
485
  requestOptions.headers = headers;
463
486
  channels.request.publish({
464
487
  request: reqMeta,
@@ -511,8 +534,6 @@ export class HttpClient extends EventEmitter {
511
534
 
512
535
  let data: any = null;
513
536
  if (args.dataType === 'stream') {
514
- // streaming mode will disable retry
515
- args.retry = 0;
516
537
  // only auto decompress on request args.compressed = true
517
538
  if (args.compressed === true && isCompressedContent) {
518
539
  // gzip or br
@@ -522,8 +543,6 @@ export class HttpClient extends EventEmitter {
522
543
  res = Object.assign(response.body, res);
523
544
  }
524
545
  } else if (args.writeStream) {
525
- // streaming mode will disable retry
526
- args.retry = 0;
527
546
  if (args.compressed === true && isCompressedContent) {
528
547
  const decoder = contentEncoding === 'gzip' ? createGunzip() : createBrotliDecompress();
529
548
  await pipelinePromise(response.body, decoder, args.writeStream);
@@ -608,11 +627,8 @@ export class HttpClient extends EventEmitter {
608
627
  err = new HttpClientRequestTimeoutError(bodyTimeout, { cause: e });
609
628
  } else if (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET') {
610
629
  // 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++;
630
+ if (args.socketErrorRetry > 0 && requestContext.socketErrorRetries < args.socketErrorRetry) {
631
+ requestContext.socketErrorRetries++;
616
632
  return await this.#requestInternal(url, options, requestContext);
617
633
  }
618
634
  }
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 true.
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
  /**
package/src/Response.ts CHANGED
@@ -49,6 +49,8 @@ export type RawResponseWithMeta = Readable & {
49
49
  rt: number;
50
50
  keepAliveSocket: boolean;
51
51
  requestUrls: string[];
52
+ retries: number;
53
+ socketErrorRetries: number;
52
54
  };
53
55
 
54
56
  export type HttpClientResponse<T = any> = {
@@ -70,7 +70,7 @@ class HttpClientRequestTimeoutError extends Error {
70
70
  Error.captureStackTrace(this, this.constructor);
71
71
  }
72
72
  }
73
- exports.HEADER_USER_AGENT = (0, default_user_agent_1.default)('node-urllib', '3.17.0');
73
+ exports.HEADER_USER_AGENT = (0, default_user_agent_1.default)('node-urllib', '3.17.1');
74
74
  function getFileName(stream) {
75
75
  const filePath = stream.path;
76
76
  if (filePath) {
@@ -131,6 +131,7 @@ class HttpClient extends node_events_1.EventEmitter {
131
131
  const headers = {};
132
132
  const args = {
133
133
  retry: 0,
134
+ socketErrorRetry: 1,
134
135
  timing: true,
135
136
  ...this.#defaultArgs,
136
137
  ...options,
@@ -140,6 +141,7 @@ class HttpClient extends node_events_1.EventEmitter {
140
141
  };
141
142
  requestContext = {
142
143
  retries: 0,
144
+ socketErrorRetries: 0,
143
145
  ...requestContext,
144
146
  };
145
147
  if (!requestContext.requestStartTime) {
@@ -205,6 +207,8 @@ class HttpClient extends node_events_1.EventEmitter {
205
207
  requestUrls: [],
206
208
  timing,
207
209
  socket: socketInfo,
210
+ retries: requestContext.retries,
211
+ socketErrorRetries: requestContext.socketErrorRetries,
208
212
  };
209
213
  let headersTimeout = 5000;
210
214
  let bodyTimeout = 5000;
@@ -249,9 +253,17 @@ class HttpClient extends node_events_1.EventEmitter {
249
253
  if (requestContext.retries > 0) {
250
254
  headers['x-urllib-retry'] = `${requestContext.retries}/${args.retry}`;
251
255
  }
256
+ if (requestContext.socketErrorRetries > 0) {
257
+ headers['x-urllib-retry-on-socket-error'] = `${requestContext.socketErrorRetries}/${args.socketErrorRetry}`;
258
+ }
252
259
  if (args.auth && !headers.authorization) {
253
260
  headers.authorization = `Basic ${Buffer.from(args.auth).toString('base64')}`;
254
261
  }
262
+ // streaming request should disable socketErrorRetry and retry
263
+ let isStreamingRequest = false;
264
+ if (args.dataType === 'stream' || args.writeStream) {
265
+ isStreamingRequest = true;
266
+ }
255
267
  try {
256
268
  const requestOptions = {
257
269
  method,
@@ -279,10 +291,12 @@ class HttpClient extends node_events_1.EventEmitter {
279
291
  if ((0, utils_1.isReadable)(args.stream) && !(args.stream instanceof node_stream_1.Readable)) {
280
292
  debug('Request#%d convert old style stream to Readable', requestId);
281
293
  args.stream = new node_stream_1.Readable().wrap(args.stream);
294
+ isStreamingRequest = true;
282
295
  }
283
296
  else if (args.stream instanceof formstream_1.default) {
284
297
  debug('Request#%d convert formstream to Readable', requestId);
285
298
  args.stream = new node_stream_1.Readable().wrap(args.stream);
299
+ isStreamingRequest = true;
286
300
  }
287
301
  args.content = args.stream;
288
302
  }
@@ -330,6 +344,7 @@ class HttpClient extends node_events_1.EventEmitter {
330
344
  else if (file instanceof node_stream_1.Readable || (0, utils_1.isReadable)(file)) {
331
345
  const fileName = getFileName(file) || `streamfile${index}`;
332
346
  formData.append(field, new BlobFromStream(file, mime_types_1.default.lookup(fileName) || ''), fileName);
347
+ isStreamingRequest = true;
333
348
  }
334
349
  }
335
350
  if (undici_1.FormData) {
@@ -355,6 +370,7 @@ class HttpClient extends node_events_1.EventEmitter {
355
370
  else if (typeof args.content === 'string' && !headers['content-type']) {
356
371
  headers['content-type'] = 'text/plain;charset=UTF-8';
357
372
  }
373
+ isStreamingRequest = (0, utils_1.isReadable)(args.content);
358
374
  }
359
375
  }
360
376
  else if (args.data) {
@@ -374,6 +390,7 @@ class HttpClient extends node_events_1.EventEmitter {
374
390
  else {
375
391
  if (isStringOrBufferOrReadable) {
376
392
  requestOptions.body = args.data;
393
+ isStreamingRequest = (0, utils_1.isReadable)(args.data);
377
394
  }
378
395
  else {
379
396
  if (args.contentType === 'json'
@@ -391,7 +408,11 @@ class HttpClient extends node_events_1.EventEmitter {
391
408
  }
392
409
  }
393
410
  }
394
- 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);
395
416
  requestOptions.headers = headers;
396
417
  channels.request.publish({
397
418
  request: reqMeta,
@@ -440,8 +461,6 @@ class HttpClient extends node_events_1.EventEmitter {
440
461
  }
441
462
  let data = null;
442
463
  if (args.dataType === 'stream') {
443
- // streaming mode will disable retry
444
- args.retry = 0;
445
464
  // only auto decompress on request args.compressed = true
446
465
  if (args.compressed === true && isCompressedContent) {
447
466
  // gzip or br
@@ -453,8 +472,6 @@ class HttpClient extends node_events_1.EventEmitter {
453
472
  }
454
473
  }
455
474
  else if (args.writeStream) {
456
- // streaming mode will disable retry
457
- args.retry = 0;
458
475
  if (args.compressed === true && isCompressedContent) {
459
476
  const decoder = contentEncoding === 'gzip' ? (0, node_zlib_1.createGunzip)() : (0, node_zlib_1.createBrotliDecompress)();
460
477
  await pipelinePromise(response.body, decoder, args.writeStream);
@@ -543,11 +560,8 @@ class HttpClient extends node_events_1.EventEmitter {
543
560
  }
544
561
  else if (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET') {
545
562
  // 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++;
563
+ if (args.socketErrorRetry > 0 && requestContext.socketErrorRetries < args.socketErrorRetry) {
564
+ requestContext.socketErrorRetries++;
551
565
  return await this.#requestInternal(url, options, requestContext);
552
566
  }
553
567
  }
@@ -67,11 +67,16 @@ export type RequestOptions = {
67
67
  headers?: IncomingHttpHeaders;
68
68
  /**
69
69
  * Request timeout in milliseconds for connecting phase and response receiving phase.
70
- * Defaults to exports.
71
- * TIMEOUT, both are 5s. You can use timeout: 5000 to tell urllib use same timeout on two phase or set them seperately such as
70
+ * 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
72
71
  * timeout: [3000, 5000], which will set connecting timeout to 3s and response 5s.
73
72
  */
74
73
  timeout?: number | number[];
74
+ /**
75
+ * Default is `4000`, 4 seconds - The timeout after which a socket without active requests will time out.
76
+ * Monitors time between activity on a connected socket.
77
+ * 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.
78
+ */
79
+ keepAliveTimeout?: number;
75
80
  /**
76
81
  * username:password used in HTTP Basic Authorization.
77
82
  * Alias to `headers.authorization = xxx`
@@ -89,7 +94,7 @@ export type RequestOptions = {
89
94
  formatRedirectUrl?: (a: any, b: any) => void;
90
95
  /** Before request hook, you can change every thing here. */
91
96
  beforeRequest?: (...args: any[]) => void;
92
- /** Accept `gzip, br` response content and auto decode it, default is false. */
97
+ /** Accept `gzip, br` response content and auto decode it, default is `true`. */
93
98
  compressed?: boolean;
94
99
  /**
95
100
  * @deprecated
@@ -97,11 +102,11 @@ export type RequestOptions = {
97
102
  * */
98
103
  gzip?: boolean;
99
104
  /**
100
- * Enable timing or not, default is true.
105
+ * Enable timing or not, default is `true`.
101
106
  * */
102
107
  timing?: boolean;
103
108
  /**
104
- * Auto retry times on 5xx response, default is 0. Don't work on streaming request
109
+ * Auto retry times on 5xx response, default is `0`. Don't work on streaming request
105
110
  * It's not supported by using retry and writeStream, because the retry request can't stop the stream which is consuming.
106
111
  **/
107
112
  retry?: number;
@@ -112,6 +117,11 @@ export type RequestOptions = {
112
117
  * It will retry when status >= 500 by default. Request error is not included.
113
118
  */
114
119
  isRetry?: (response: HttpClientResponse) => boolean;
120
+ /**
121
+ * Auto retry times on socket error, default is `1`. Don't work on streaming request
122
+ * It's not supported by using retry and writeStream, because the retry request can't stop the stream which is consuming.
123
+ **/
124
+ socketErrorRetry?: number;
115
125
  /** Default: `null` */
116
126
  opaque?: unknown;
117
127
  /**
@@ -39,6 +39,8 @@ export type RawResponseWithMeta = Readable & {
39
39
  rt: number;
40
40
  keepAliveSocket: boolean;
41
41
  requestUrls: string[];
42
+ retries: number;
43
+ socketErrorRetries: number;
42
44
  };
43
45
  export type HttpClientResponse<T = any> = {
44
46
  opaque: unknown;
@@ -64,7 +64,7 @@ class HttpClientRequestTimeoutError extends Error {
64
64
  Error.captureStackTrace(this, this.constructor);
65
65
  }
66
66
  }
67
- export const HEADER_USER_AGENT = createUserAgent('node-urllib', '3.17.0');
67
+ export const HEADER_USER_AGENT = createUserAgent('node-urllib', '3.17.1');
68
68
  function getFileName(stream) {
69
69
  const filePath = stream.path;
70
70
  if (filePath) {
@@ -125,6 +125,7 @@ export class HttpClient extends EventEmitter {
125
125
  const headers = {};
126
126
  const args = {
127
127
  retry: 0,
128
+ socketErrorRetry: 1,
128
129
  timing: true,
129
130
  ...this.#defaultArgs,
130
131
  ...options,
@@ -134,6 +135,7 @@ export class HttpClient extends EventEmitter {
134
135
  };
135
136
  requestContext = {
136
137
  retries: 0,
138
+ socketErrorRetries: 0,
137
139
  ...requestContext,
138
140
  };
139
141
  if (!requestContext.requestStartTime) {
@@ -199,6 +201,8 @@ export class HttpClient extends EventEmitter {
199
201
  requestUrls: [],
200
202
  timing,
201
203
  socket: socketInfo,
204
+ retries: requestContext.retries,
205
+ socketErrorRetries: requestContext.socketErrorRetries,
202
206
  };
203
207
  let headersTimeout = 5000;
204
208
  let bodyTimeout = 5000;
@@ -243,9 +247,17 @@ export class HttpClient extends EventEmitter {
243
247
  if (requestContext.retries > 0) {
244
248
  headers['x-urllib-retry'] = `${requestContext.retries}/${args.retry}`;
245
249
  }
250
+ if (requestContext.socketErrorRetries > 0) {
251
+ headers['x-urllib-retry-on-socket-error'] = `${requestContext.socketErrorRetries}/${args.socketErrorRetry}`;
252
+ }
246
253
  if (args.auth && !headers.authorization) {
247
254
  headers.authorization = `Basic ${Buffer.from(args.auth).toString('base64')}`;
248
255
  }
256
+ // streaming request should disable socketErrorRetry and retry
257
+ let isStreamingRequest = false;
258
+ if (args.dataType === 'stream' || args.writeStream) {
259
+ isStreamingRequest = true;
260
+ }
249
261
  try {
250
262
  const requestOptions = {
251
263
  method,
@@ -273,10 +285,12 @@ export class HttpClient extends EventEmitter {
273
285
  if (isReadable(args.stream) && !(args.stream instanceof Readable)) {
274
286
  debug('Request#%d convert old style stream to Readable', requestId);
275
287
  args.stream = new Readable().wrap(args.stream);
288
+ isStreamingRequest = true;
276
289
  }
277
290
  else if (args.stream instanceof FormStream) {
278
291
  debug('Request#%d convert formstream to Readable', requestId);
279
292
  args.stream = new Readable().wrap(args.stream);
293
+ isStreamingRequest = true;
280
294
  }
281
295
  args.content = args.stream;
282
296
  }
@@ -324,6 +338,7 @@ export class HttpClient extends EventEmitter {
324
338
  else if (file instanceof Readable || isReadable(file)) {
325
339
  const fileName = getFileName(file) || `streamfile${index}`;
326
340
  formData.append(field, new BlobFromStream(file, mime.lookup(fileName) || ''), fileName);
341
+ isStreamingRequest = true;
327
342
  }
328
343
  }
329
344
  if (FormDataNative) {
@@ -349,6 +364,7 @@ export class HttpClient extends EventEmitter {
349
364
  else if (typeof args.content === 'string' && !headers['content-type']) {
350
365
  headers['content-type'] = 'text/plain;charset=UTF-8';
351
366
  }
367
+ isStreamingRequest = isReadable(args.content);
352
368
  }
353
369
  }
354
370
  else if (args.data) {
@@ -368,6 +384,7 @@ export class HttpClient extends EventEmitter {
368
384
  else {
369
385
  if (isStringOrBufferOrReadable) {
370
386
  requestOptions.body = args.data;
387
+ isStreamingRequest = isReadable(args.data);
371
388
  }
372
389
  else {
373
390
  if (args.contentType === 'json'
@@ -385,7 +402,11 @@ export class HttpClient extends EventEmitter {
385
402
  }
386
403
  }
387
404
  }
388
- debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s', requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout);
405
+ if (isStreamingRequest) {
406
+ args.retry = 0;
407
+ args.socketErrorRetry = 0;
408
+ }
409
+ debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s, isStreamingRequest: %s', requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout, isStreamingRequest);
389
410
  requestOptions.headers = headers;
390
411
  channels.request.publish({
391
412
  request: reqMeta,
@@ -434,8 +455,6 @@ export class HttpClient extends EventEmitter {
434
455
  }
435
456
  let data = null;
436
457
  if (args.dataType === 'stream') {
437
- // streaming mode will disable retry
438
- args.retry = 0;
439
458
  // only auto decompress on request args.compressed = true
440
459
  if (args.compressed === true && isCompressedContent) {
441
460
  // gzip or br
@@ -447,8 +466,6 @@ export class HttpClient extends EventEmitter {
447
466
  }
448
467
  }
449
468
  else if (args.writeStream) {
450
- // streaming mode will disable retry
451
- args.retry = 0;
452
469
  if (args.compressed === true && isCompressedContent) {
453
470
  const decoder = contentEncoding === 'gzip' ? createGunzip() : createBrotliDecompress();
454
471
  await pipelinePromise(response.body, decoder, args.writeStream);
@@ -537,11 +554,8 @@ export class HttpClient extends EventEmitter {
537
554
  }
538
555
  else if (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET') {
539
556
  // 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++;
557
+ if (args.socketErrorRetry > 0 && requestContext.socketErrorRetries < args.socketErrorRetry) {
558
+ requestContext.socketErrorRetries++;
545
559
  return await this.#requestInternal(url, options, requestContext);
546
560
  }
547
561
  }
@@ -67,11 +67,16 @@ export type RequestOptions = {
67
67
  headers?: IncomingHttpHeaders;
68
68
  /**
69
69
  * Request timeout in milliseconds for connecting phase and response receiving phase.
70
- * Defaults to exports.
71
- * TIMEOUT, both are 5s. You can use timeout: 5000 to tell urllib use same timeout on two phase or set them seperately such as
70
+ * 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
72
71
  * timeout: [3000, 5000], which will set connecting timeout to 3s and response 5s.
73
72
  */
74
73
  timeout?: number | number[];
74
+ /**
75
+ * Default is `4000`, 4 seconds - The timeout after which a socket without active requests will time out.
76
+ * Monitors time between activity on a connected socket.
77
+ * 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.
78
+ */
79
+ keepAliveTimeout?: number;
75
80
  /**
76
81
  * username:password used in HTTP Basic Authorization.
77
82
  * Alias to `headers.authorization = xxx`
@@ -89,7 +94,7 @@ export type RequestOptions = {
89
94
  formatRedirectUrl?: (a: any, b: any) => void;
90
95
  /** Before request hook, you can change every thing here. */
91
96
  beforeRequest?: (...args: any[]) => void;
92
- /** Accept `gzip, br` response content and auto decode it, default is false. */
97
+ /** Accept `gzip, br` response content and auto decode it, default is `true`. */
93
98
  compressed?: boolean;
94
99
  /**
95
100
  * @deprecated
@@ -97,11 +102,11 @@ export type RequestOptions = {
97
102
  * */
98
103
  gzip?: boolean;
99
104
  /**
100
- * Enable timing or not, default is true.
105
+ * Enable timing or not, default is `true`.
101
106
  * */
102
107
  timing?: boolean;
103
108
  /**
104
- * Auto retry times on 5xx response, default is 0. Don't work on streaming request
109
+ * Auto retry times on 5xx response, default is `0`. Don't work on streaming request
105
110
  * It's not supported by using retry and writeStream, because the retry request can't stop the stream which is consuming.
106
111
  **/
107
112
  retry?: number;
@@ -112,6 +117,11 @@ export type RequestOptions = {
112
117
  * It will retry when status >= 500 by default. Request error is not included.
113
118
  */
114
119
  isRetry?: (response: HttpClientResponse) => boolean;
120
+ /**
121
+ * Auto retry times on socket error, default is `1`. Don't work on streaming request
122
+ * It's not supported by using retry and writeStream, because the retry request can't stop the stream which is consuming.
123
+ **/
124
+ socketErrorRetry?: number;
115
125
  /** Default: `null` */
116
126
  opaque?: unknown;
117
127
  /**
@@ -39,6 +39,8 @@ export type RawResponseWithMeta = Readable & {
39
39
  rt: number;
40
40
  keepAliveSocket: boolean;
41
41
  requestUrls: string[];
42
+ retries: number;
43
+ socketErrorRetries: number;
42
44
  };
43
45
  export type HttpClientResponse<T = any> = {
44
46
  opaque: unknown;