urllib 3.17.0 → 3.17.2

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.2",
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/HttpAgent.ts CHANGED
@@ -37,8 +37,11 @@ export class HttpAgent extends Agent {
37
37
  /* eslint node/prefer-promises/dns: off*/
38
38
  const _lookup = options.lookup ?? dns.lookup;
39
39
  const lookup: LookupFunction = (hostname, dnsOptions, callback) => {
40
- _lookup(hostname, dnsOptions, (err, address, family) => {
41
- if (err) return callback(err, address, family);
40
+ _lookup(hostname, dnsOptions, (err, ...args: any[]) => {
41
+ // address will be array on Node.js >= 20
42
+ const address = args[0];
43
+ const family = args[1];
44
+ if (err) return (callback as any)(err, address, family);
42
45
  if (options.checkAddress) {
43
46
  // dnsOptions.all set to default on Node.js >= 20, dns.lookup will return address array object
44
47
  if (typeof address === 'string') {
@@ -55,7 +58,7 @@ export class HttpAgent extends Agent {
55
58
  }
56
59
  }
57
60
  }
58
- callback(err, address, family);
61
+ (callback as any)(err, address, family);
59
62
  });
60
63
  };
61
64
  super({
package/src/HttpClient.ts CHANGED
@@ -58,6 +58,8 @@ function noop() {
58
58
  }
59
59
 
60
60
  const debug = debuglog('urllib:HttpClient');
61
+ // Node.js 14 or 16
62
+ const isNode14Or16 = /v1[46]\./.test(process.version);
61
63
 
62
64
  export type ClientOptions = {
63
65
  defaultArgs?: RequestOptions;
@@ -138,6 +140,7 @@ function defaultIsRetry(response: HttpClientResponse) {
138
140
 
139
141
  type RequestContext = {
140
142
  retries: number;
143
+ socketErrorRetries: number;
141
144
  requestStartTime?: number;
142
145
  };
143
146
 
@@ -206,6 +209,7 @@ export class HttpClient extends EventEmitter {
206
209
  const headers: IncomingHttpHeaders = {};
207
210
  const args = {
208
211
  retry: 0,
212
+ socketErrorRetry: 1,
209
213
  timing: true,
210
214
  ...this.#defaultArgs,
211
215
  ...options,
@@ -215,6 +219,7 @@ export class HttpClient extends EventEmitter {
215
219
  };
216
220
  requestContext = {
217
221
  retries: 0,
222
+ socketErrorRetries: 0,
218
223
  ...requestContext,
219
224
  };
220
225
  if (!requestContext.requestStartTime) {
@@ -281,6 +286,8 @@ export class HttpClient extends EventEmitter {
281
286
  requestUrls: [],
282
287
  timing,
283
288
  socket: socketInfo,
289
+ retries: requestContext.retries,
290
+ socketErrorRetries: requestContext.socketErrorRetries,
284
291
  } as any as RawResponseWithMeta;
285
292
 
286
293
  let headersTimeout = 5000;
@@ -324,10 +331,19 @@ export class HttpClient extends EventEmitter {
324
331
  if (requestContext.retries > 0) {
325
332
  headers['x-urllib-retry'] = `${requestContext.retries}/${args.retry}`;
326
333
  }
334
+ if (requestContext.socketErrorRetries > 0) {
335
+ headers['x-urllib-retry-on-socket-error'] = `${requestContext.socketErrorRetries}/${args.socketErrorRetry}`;
336
+ }
327
337
  if (args.auth && !headers.authorization) {
328
338
  headers.authorization = `Basic ${Buffer.from(args.auth).toString('base64')}`;
329
339
  }
330
340
 
341
+ // streaming request should disable socketErrorRetry and retry
342
+ let isStreamingRequest = false;
343
+ if (args.dataType === 'stream' || args.writeStream) {
344
+ isStreamingRequest = true;
345
+ }
346
+
331
347
  try {
332
348
  const requestOptions: IUndiciRequestOption = {
333
349
  method,
@@ -356,9 +372,11 @@ export class HttpClient extends EventEmitter {
356
372
  if (isReadable(args.stream) && !(args.stream instanceof Readable)) {
357
373
  debug('Request#%d convert old style stream to Readable', requestId);
358
374
  args.stream = new Readable().wrap(args.stream);
375
+ isStreamingRequest = true;
359
376
  } else if (args.stream instanceof FormStream) {
360
377
  debug('Request#%d convert formstream to Readable', requestId);
361
378
  args.stream = new Readable().wrap(args.stream);
379
+ isStreamingRequest = true;
362
380
  }
363
381
  args.content = args.stream;
364
382
  }
@@ -402,6 +420,7 @@ export class HttpClient extends EventEmitter {
402
420
  } else if (file instanceof Readable || isReadable(file as any)) {
403
421
  const fileName = getFileName(file) || `streamfile${index}`;
404
422
  formData.append(field, new BlobFromStream(file, mime.lookup(fileName) || ''), fileName);
423
+ isStreamingRequest = true;
405
424
  }
406
425
  }
407
426
 
@@ -425,6 +444,7 @@ export class HttpClient extends EventEmitter {
425
444
  } else if (typeof args.content === 'string' && !headers['content-type']) {
426
445
  headers['content-type'] = 'text/plain;charset=UTF-8';
427
446
  }
447
+ isStreamingRequest = isReadable(args.content);
428
448
  }
429
449
  } else if (args.data) {
430
450
  const isStringOrBufferOrReadable = typeof args.data === 'string'
@@ -441,6 +461,7 @@ export class HttpClient extends EventEmitter {
441
461
  } else {
442
462
  if (isStringOrBufferOrReadable) {
443
463
  requestOptions.body = args.data;
464
+ isStreamingRequest = isReadable(args.data);
444
465
  } else {
445
466
  if (args.contentType === 'json'
446
467
  || args.contentType === 'application/json'
@@ -456,9 +477,13 @@ export class HttpClient extends EventEmitter {
456
477
  }
457
478
  }
458
479
  }
480
+ if (isStreamingRequest) {
481
+ args.retry = 0;
482
+ args.socketErrorRetry = 0;
483
+ }
459
484
 
460
- debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s',
461
- requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout);
485
+ debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s, isStreamingRequest: %s',
486
+ requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout, isStreamingRequest);
462
487
  requestOptions.headers = headers;
463
488
  channels.request.publish({
464
489
  request: reqMeta,
@@ -511,8 +536,6 @@ export class HttpClient extends EventEmitter {
511
536
 
512
537
  let data: any = null;
513
538
  if (args.dataType === 'stream') {
514
- // streaming mode will disable retry
515
- args.retry = 0;
516
539
  // only auto decompress on request args.compressed = true
517
540
  if (args.compressed === true && isCompressedContent) {
518
541
  // gzip or br
@@ -522,8 +545,9 @@ export class HttpClient extends EventEmitter {
522
545
  res = Object.assign(response.body, res);
523
546
  }
524
547
  } else if (args.writeStream) {
525
- // streaming mode will disable retry
526
- args.retry = 0;
548
+ if (isNode14Or16 && args.writeStream.destroyed) {
549
+ throw new Error('writeStream is destroyed');
550
+ }
527
551
  if (args.compressed === true && isCompressedContent) {
528
552
  const decoder = contentEncoding === 'gzip' ? createGunzip() : createBrotliDecompress();
529
553
  await pipelinePromise(response.body, decoder, args.writeStream);
@@ -608,11 +632,8 @@ export class HttpClient extends EventEmitter {
608
632
  err = new HttpClientRequestTimeoutError(bodyTimeout, { cause: e });
609
633
  } else if (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET') {
610
634
  // 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++;
635
+ if (args.socketErrorRetry > 0 && requestContext.socketErrorRetries < args.socketErrorRetry) {
636
+ requestContext.socketErrorRetries++;
616
637
  return await this.#requestInternal(url, options, requestContext);
617
638
  }
618
639
  }
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> = {
@@ -27,7 +27,10 @@ class HttpAgent extends undici_1.Agent {
27
27
  /* eslint node/prefer-promises/dns: off*/
28
28
  const _lookup = options.lookup ?? node_dns_1.default.lookup;
29
29
  const lookup = (hostname, dnsOptions, callback) => {
30
- _lookup(hostname, dnsOptions, (err, address, family) => {
30
+ _lookup(hostname, dnsOptions, (err, ...args) => {
31
+ // address will be array on Node.js >= 20
32
+ const address = args[0];
33
+ const family = args[1];
31
34
  if (err)
32
35
  return callback(err, address, family);
33
36
  if (options.checkAddress) {
@@ -44,6 +44,8 @@ function noop() {
44
44
  // noop
45
45
  }
46
46
  const debug = (0, node_util_1.debuglog)('urllib:HttpClient');
47
+ // Node.js 14 or 16
48
+ const isNode14Or16 = /v1[46]\./.test(process.version);
47
49
  // https://github.com/octet-stream/form-data
48
50
  class BlobFromStream {
49
51
  #stream;
@@ -70,7 +72,7 @@ class HttpClientRequestTimeoutError extends Error {
70
72
  Error.captureStackTrace(this, this.constructor);
71
73
  }
72
74
  }
73
- exports.HEADER_USER_AGENT = (0, default_user_agent_1.default)('node-urllib', '3.17.0');
75
+ exports.HEADER_USER_AGENT = (0, default_user_agent_1.default)('node-urllib', '3.17.2');
74
76
  function getFileName(stream) {
75
77
  const filePath = stream.path;
76
78
  if (filePath) {
@@ -131,6 +133,7 @@ class HttpClient extends node_events_1.EventEmitter {
131
133
  const headers = {};
132
134
  const args = {
133
135
  retry: 0,
136
+ socketErrorRetry: 1,
134
137
  timing: true,
135
138
  ...this.#defaultArgs,
136
139
  ...options,
@@ -140,6 +143,7 @@ class HttpClient extends node_events_1.EventEmitter {
140
143
  };
141
144
  requestContext = {
142
145
  retries: 0,
146
+ socketErrorRetries: 0,
143
147
  ...requestContext,
144
148
  };
145
149
  if (!requestContext.requestStartTime) {
@@ -205,6 +209,8 @@ class HttpClient extends node_events_1.EventEmitter {
205
209
  requestUrls: [],
206
210
  timing,
207
211
  socket: socketInfo,
212
+ retries: requestContext.retries,
213
+ socketErrorRetries: requestContext.socketErrorRetries,
208
214
  };
209
215
  let headersTimeout = 5000;
210
216
  let bodyTimeout = 5000;
@@ -249,9 +255,17 @@ class HttpClient extends node_events_1.EventEmitter {
249
255
  if (requestContext.retries > 0) {
250
256
  headers['x-urllib-retry'] = `${requestContext.retries}/${args.retry}`;
251
257
  }
258
+ if (requestContext.socketErrorRetries > 0) {
259
+ headers['x-urllib-retry-on-socket-error'] = `${requestContext.socketErrorRetries}/${args.socketErrorRetry}`;
260
+ }
252
261
  if (args.auth && !headers.authorization) {
253
262
  headers.authorization = `Basic ${Buffer.from(args.auth).toString('base64')}`;
254
263
  }
264
+ // streaming request should disable socketErrorRetry and retry
265
+ let isStreamingRequest = false;
266
+ if (args.dataType === 'stream' || args.writeStream) {
267
+ isStreamingRequest = true;
268
+ }
255
269
  try {
256
270
  const requestOptions = {
257
271
  method,
@@ -279,10 +293,12 @@ class HttpClient extends node_events_1.EventEmitter {
279
293
  if ((0, utils_1.isReadable)(args.stream) && !(args.stream instanceof node_stream_1.Readable)) {
280
294
  debug('Request#%d convert old style stream to Readable', requestId);
281
295
  args.stream = new node_stream_1.Readable().wrap(args.stream);
296
+ isStreamingRequest = true;
282
297
  }
283
298
  else if (args.stream instanceof formstream_1.default) {
284
299
  debug('Request#%d convert formstream to Readable', requestId);
285
300
  args.stream = new node_stream_1.Readable().wrap(args.stream);
301
+ isStreamingRequest = true;
286
302
  }
287
303
  args.content = args.stream;
288
304
  }
@@ -330,6 +346,7 @@ class HttpClient extends node_events_1.EventEmitter {
330
346
  else if (file instanceof node_stream_1.Readable || (0, utils_1.isReadable)(file)) {
331
347
  const fileName = getFileName(file) || `streamfile${index}`;
332
348
  formData.append(field, new BlobFromStream(file, mime_types_1.default.lookup(fileName) || ''), fileName);
349
+ isStreamingRequest = true;
333
350
  }
334
351
  }
335
352
  if (undici_1.FormData) {
@@ -355,6 +372,7 @@ class HttpClient extends node_events_1.EventEmitter {
355
372
  else if (typeof args.content === 'string' && !headers['content-type']) {
356
373
  headers['content-type'] = 'text/plain;charset=UTF-8';
357
374
  }
375
+ isStreamingRequest = (0, utils_1.isReadable)(args.content);
358
376
  }
359
377
  }
360
378
  else if (args.data) {
@@ -374,6 +392,7 @@ class HttpClient extends node_events_1.EventEmitter {
374
392
  else {
375
393
  if (isStringOrBufferOrReadable) {
376
394
  requestOptions.body = args.data;
395
+ isStreamingRequest = (0, utils_1.isReadable)(args.data);
377
396
  }
378
397
  else {
379
398
  if (args.contentType === 'json'
@@ -391,7 +410,11 @@ class HttpClient extends node_events_1.EventEmitter {
391
410
  }
392
411
  }
393
412
  }
394
- debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s', requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout);
413
+ if (isStreamingRequest) {
414
+ args.retry = 0;
415
+ args.socketErrorRetry = 0;
416
+ }
417
+ debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s, isStreamingRequest: %s', requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout, isStreamingRequest);
395
418
  requestOptions.headers = headers;
396
419
  channels.request.publish({
397
420
  request: reqMeta,
@@ -440,8 +463,6 @@ class HttpClient extends node_events_1.EventEmitter {
440
463
  }
441
464
  let data = null;
442
465
  if (args.dataType === 'stream') {
443
- // streaming mode will disable retry
444
- args.retry = 0;
445
466
  // only auto decompress on request args.compressed = true
446
467
  if (args.compressed === true && isCompressedContent) {
447
468
  // gzip or br
@@ -453,8 +474,9 @@ class HttpClient extends node_events_1.EventEmitter {
453
474
  }
454
475
  }
455
476
  else if (args.writeStream) {
456
- // streaming mode will disable retry
457
- args.retry = 0;
477
+ if (isNode14Or16 && args.writeStream.destroyed) {
478
+ throw new Error('writeStream is destroyed');
479
+ }
458
480
  if (args.compressed === true && isCompressedContent) {
459
481
  const decoder = contentEncoding === 'gzip' ? (0, node_zlib_1.createGunzip)() : (0, node_zlib_1.createBrotliDecompress)();
460
482
  await pipelinePromise(response.body, decoder, args.writeStream);
@@ -543,11 +565,8 @@ class HttpClient extends node_events_1.EventEmitter {
543
565
  }
544
566
  else if (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET') {
545
567
  // 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++;
568
+ if (args.socketErrorRetry > 0 && requestContext.socketErrorRetries < args.socketErrorRetry) {
569
+ requestContext.socketErrorRetries++;
551
570
  return await this.#requestInternal(url, options, requestContext);
552
571
  }
553
572
  }
@@ -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;
@@ -21,7 +21,10 @@ export class HttpAgent extends Agent {
21
21
  /* eslint node/prefer-promises/dns: off*/
22
22
  const _lookup = options.lookup ?? dns.lookup;
23
23
  const lookup = (hostname, dnsOptions, callback) => {
24
- _lookup(hostname, dnsOptions, (err, address, family) => {
24
+ _lookup(hostname, dnsOptions, (err, ...args) => {
25
+ // address will be array on Node.js >= 20
26
+ const address = args[0];
27
+ const family = args[1];
25
28
  if (err)
26
29
  return callback(err, address, family);
27
30
  if (options.checkAddress) {
@@ -38,6 +38,8 @@ function noop() {
38
38
  // noop
39
39
  }
40
40
  const debug = debuglog('urllib:HttpClient');
41
+ // Node.js 14 or 16
42
+ const isNode14Or16 = /v1[46]\./.test(process.version);
41
43
  // https://github.com/octet-stream/form-data
42
44
  class BlobFromStream {
43
45
  #stream;
@@ -64,7 +66,7 @@ class HttpClientRequestTimeoutError extends Error {
64
66
  Error.captureStackTrace(this, this.constructor);
65
67
  }
66
68
  }
67
- export const HEADER_USER_AGENT = createUserAgent('node-urllib', '3.17.0');
69
+ export const HEADER_USER_AGENT = createUserAgent('node-urllib', '3.17.2');
68
70
  function getFileName(stream) {
69
71
  const filePath = stream.path;
70
72
  if (filePath) {
@@ -125,6 +127,7 @@ export class HttpClient extends EventEmitter {
125
127
  const headers = {};
126
128
  const args = {
127
129
  retry: 0,
130
+ socketErrorRetry: 1,
128
131
  timing: true,
129
132
  ...this.#defaultArgs,
130
133
  ...options,
@@ -134,6 +137,7 @@ export class HttpClient extends EventEmitter {
134
137
  };
135
138
  requestContext = {
136
139
  retries: 0,
140
+ socketErrorRetries: 0,
137
141
  ...requestContext,
138
142
  };
139
143
  if (!requestContext.requestStartTime) {
@@ -199,6 +203,8 @@ export class HttpClient extends EventEmitter {
199
203
  requestUrls: [],
200
204
  timing,
201
205
  socket: socketInfo,
206
+ retries: requestContext.retries,
207
+ socketErrorRetries: requestContext.socketErrorRetries,
202
208
  };
203
209
  let headersTimeout = 5000;
204
210
  let bodyTimeout = 5000;
@@ -243,9 +249,17 @@ export class HttpClient extends EventEmitter {
243
249
  if (requestContext.retries > 0) {
244
250
  headers['x-urllib-retry'] = `${requestContext.retries}/${args.retry}`;
245
251
  }
252
+ if (requestContext.socketErrorRetries > 0) {
253
+ headers['x-urllib-retry-on-socket-error'] = `${requestContext.socketErrorRetries}/${args.socketErrorRetry}`;
254
+ }
246
255
  if (args.auth && !headers.authorization) {
247
256
  headers.authorization = `Basic ${Buffer.from(args.auth).toString('base64')}`;
248
257
  }
258
+ // streaming request should disable socketErrorRetry and retry
259
+ let isStreamingRequest = false;
260
+ if (args.dataType === 'stream' || args.writeStream) {
261
+ isStreamingRequest = true;
262
+ }
249
263
  try {
250
264
  const requestOptions = {
251
265
  method,
@@ -273,10 +287,12 @@ export class HttpClient extends EventEmitter {
273
287
  if (isReadable(args.stream) && !(args.stream instanceof Readable)) {
274
288
  debug('Request#%d convert old style stream to Readable', requestId);
275
289
  args.stream = new Readable().wrap(args.stream);
290
+ isStreamingRequest = true;
276
291
  }
277
292
  else if (args.stream instanceof FormStream) {
278
293
  debug('Request#%d convert formstream to Readable', requestId);
279
294
  args.stream = new Readable().wrap(args.stream);
295
+ isStreamingRequest = true;
280
296
  }
281
297
  args.content = args.stream;
282
298
  }
@@ -324,6 +340,7 @@ export class HttpClient extends EventEmitter {
324
340
  else if (file instanceof Readable || isReadable(file)) {
325
341
  const fileName = getFileName(file) || `streamfile${index}`;
326
342
  formData.append(field, new BlobFromStream(file, mime.lookup(fileName) || ''), fileName);
343
+ isStreamingRequest = true;
327
344
  }
328
345
  }
329
346
  if (FormDataNative) {
@@ -349,6 +366,7 @@ export class HttpClient extends EventEmitter {
349
366
  else if (typeof args.content === 'string' && !headers['content-type']) {
350
367
  headers['content-type'] = 'text/plain;charset=UTF-8';
351
368
  }
369
+ isStreamingRequest = isReadable(args.content);
352
370
  }
353
371
  }
354
372
  else if (args.data) {
@@ -368,6 +386,7 @@ export class HttpClient extends EventEmitter {
368
386
  else {
369
387
  if (isStringOrBufferOrReadable) {
370
388
  requestOptions.body = args.data;
389
+ isStreamingRequest = isReadable(args.data);
371
390
  }
372
391
  else {
373
392
  if (args.contentType === 'json'
@@ -385,7 +404,11 @@ export class HttpClient extends EventEmitter {
385
404
  }
386
405
  }
387
406
  }
388
- debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s', requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout);
407
+ if (isStreamingRequest) {
408
+ args.retry = 0;
409
+ args.socketErrorRetry = 0;
410
+ }
411
+ debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s, isStreamingRequest: %s', requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout, isStreamingRequest);
389
412
  requestOptions.headers = headers;
390
413
  channels.request.publish({
391
414
  request: reqMeta,
@@ -434,8 +457,6 @@ export class HttpClient extends EventEmitter {
434
457
  }
435
458
  let data = null;
436
459
  if (args.dataType === 'stream') {
437
- // streaming mode will disable retry
438
- args.retry = 0;
439
460
  // only auto decompress on request args.compressed = true
440
461
  if (args.compressed === true && isCompressedContent) {
441
462
  // gzip or br
@@ -447,8 +468,9 @@ export class HttpClient extends EventEmitter {
447
468
  }
448
469
  }
449
470
  else if (args.writeStream) {
450
- // streaming mode will disable retry
451
- args.retry = 0;
471
+ if (isNode14Or16 && args.writeStream.destroyed) {
472
+ throw new Error('writeStream is destroyed');
473
+ }
452
474
  if (args.compressed === true && isCompressedContent) {
453
475
  const decoder = contentEncoding === 'gzip' ? createGunzip() : createBrotliDecompress();
454
476
  await pipelinePromise(response.body, decoder, args.writeStream);
@@ -537,11 +559,8 @@ export class HttpClient extends EventEmitter {
537
559
  }
538
560
  else if (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET') {
539
561
  // 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++;
562
+ if (args.socketErrorRetry > 0 && requestContext.socketErrorRetries < args.socketErrorRetry) {
563
+ requestContext.socketErrorRetries++;
545
564
  return await this.#requestInternal(url, options, requestContext);
546
565
  }
547
566
  }
@@ -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;