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 +4 -3
- package/package.json +2 -2
- package/src/HttpClient.ts +27 -11
- package/src/Request.ts +15 -5
- package/src/Response.ts +2 -0
- package/src/cjs/HttpClient.js +25 -11
- package/src/cjs/Request.d.ts +15 -5
- package/src/cjs/Response.d.ts +2 -0
- package/src/esm/HttpClient.js +25 -11
- package/src/esm/Request.d.ts +15 -5
- package/src/esm/Response.d.ts +2 -0
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.
|
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 `
|
76
|
-
- ***timing*** Boolean - Enable timing or not, default is `
|
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.
|
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
|
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.
|
612
|
-
|
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
|
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
|
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
|
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
package/src/cjs/HttpClient.js
CHANGED
@@ -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.
|
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
|
-
|
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.
|
547
|
-
|
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
|
}
|
package/src/cjs/Request.d.ts
CHANGED
@@ -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
|
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
|
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
|
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
|
/**
|
package/src/cjs/Response.d.ts
CHANGED
package/src/esm/HttpClient.js
CHANGED
@@ -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.
|
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
|
-
|
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.
|
541
|
-
|
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
|
}
|
package/src/esm/Request.d.ts
CHANGED
@@ -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
|
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
|
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
|
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
|
/**
|
package/src/esm/Response.d.ts
CHANGED