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 +4 -3
- package/package.json +2 -2
- package/src/HttpAgent.ts +6 -3
- package/src/HttpClient.ts +32 -11
- package/src/Request.ts +15 -5
- package/src/Response.ts +2 -0
- package/src/cjs/HttpAgent.js +4 -1
- package/src/cjs/HttpClient.js +30 -11
- package/src/cjs/Request.d.ts +15 -5
- package/src/cjs/Response.d.ts +2 -0
- package/src/esm/HttpAgent.js +4 -1
- package/src/esm/HttpClient.js +30 -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.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
|
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,
|
41
|
-
|
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
|
-
|
526
|
-
|
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.
|
612
|
-
|
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
|
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/HttpAgent.js
CHANGED
@@ -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,
|
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) {
|
package/src/cjs/HttpClient.js
CHANGED
@@ -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.
|
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
|
-
|
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
|
-
|
457
|
-
|
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.
|
547
|
-
|
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
|
}
|
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/HttpAgent.js
CHANGED
@@ -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,
|
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) {
|
package/src/esm/HttpClient.js
CHANGED
@@ -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.
|
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
|
-
|
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
|
-
|
451
|
-
|
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.
|
541
|
-
|
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
|
}
|
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