urllib 3.16.1 → 3.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -7
- package/package.json +5 -3
- package/src/HttpClient.ts +79 -12
- package/src/Request.ts +23 -5
- package/src/Response.ts +4 -0
- package/src/cjs/HttpClient.d.ts +10 -2
- package/src/cjs/HttpClient.js +62 -9
- package/src/cjs/Request.d.ts +22 -5
- package/src/cjs/Response.d.ts +4 -0
- package/src/cjs/diagnosticsChannel.js +42 -10
- package/src/cjs/index.d.ts +1 -1
- package/src/cjs/symbols.d.ts +4 -0
- package/src/cjs/symbols.js +4 -0
- package/src/diagnosticsChannel.ts +49 -12
- package/src/esm/HttpClient.d.ts +10 -2
- package/src/esm/HttpClient.js +62 -9
- package/src/esm/Request.d.ts +22 -5
- package/src/esm/Response.d.ts +4 -0
- package/src/esm/diagnosticsChannel.js +42 -10
- package/src/esm/index.d.ts +1 -1
- package/src/esm/index.js +1 -1
- package/src/esm/symbols.d.ts +4 -0
- package/src/esm/symbols.js +4 -0
- package/src/index.ts +4 -1
- package/src/symbols.ts +4 -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
|
|
@@ -200,10 +201,7 @@ Response is normal object, it contains:
|
|
200
201
|
- `aborted`: response was aborted or not
|
201
202
|
- `rt`: total request and response time in ms.
|
202
203
|
- `timing`: timing object if timing enable.
|
203
|
-
- `
|
204
|
-
- `remotePort`: http server ip port
|
205
|
-
- `socketHandledRequests`: socket already handled request count
|
206
|
-
- `socketHandledResponses`: socket already handled response count
|
204
|
+
- `socket`: socket info
|
207
205
|
|
208
206
|
## Run test with debug log
|
209
207
|
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "urllib",
|
3
|
-
"version": "3.
|
3
|
+
"version": "3.17.1",
|
4
4
|
"publishConfig": {
|
5
5
|
"tag": "latest"
|
6
6
|
},
|
@@ -53,6 +53,7 @@
|
|
53
53
|
"build:test": "npm run build && npm run build:cjs:test && npm run build:esm:test && npm run test-tsc",
|
54
54
|
"test-tsc": "tsc -p ./test/fixtures/ts/tsconfig.json",
|
55
55
|
"test": "npm run lint && vitest run",
|
56
|
+
"test-keepalive": "cross-env TEST_KEEPALIVE_COUNT=50 vitest run --test-timeout 180000 keep-alive-header.test.ts",
|
56
57
|
"cov": "vitest run --coverage",
|
57
58
|
"ci": "npm run lint && npm run cov && npm run build:test",
|
58
59
|
"contributor": "git-contributor",
|
@@ -78,8 +79,9 @@
|
|
78
79
|
"@types/pump": "^1.1.1",
|
79
80
|
"@types/selfsigned": "^2.0.1",
|
80
81
|
"@types/tar-stream": "^2.2.2",
|
81
|
-
"@vitest/coverage-
|
82
|
+
"@vitest/coverage-v8": "^0.32.0",
|
82
83
|
"busboy": "^1.6.0",
|
84
|
+
"cross-env": "^7.0.3",
|
83
85
|
"eslint": "^8.25.0",
|
84
86
|
"eslint-config-egg": "^12.1.0",
|
85
87
|
"git-contributor": "^2.0.0",
|
@@ -88,7 +90,7 @@
|
|
88
90
|
"selfsigned": "^2.0.1",
|
89
91
|
"tar-stream": "^2.2.0",
|
90
92
|
"typescript": "^5.0.4",
|
91
|
-
"vitest": "^0.
|
93
|
+
"vitest": "^0.32.0"
|
92
94
|
},
|
93
95
|
"engines": {
|
94
96
|
"node": ">= 14.19.3"
|
package/src/HttpClient.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import diagnosticsChannel from 'node:diagnostics_channel';
|
1
2
|
import { EventEmitter } from 'node:events';
|
2
3
|
import { LookupFunction } from 'node:net';
|
3
4
|
import { STATUS_CODES } from 'node:http';
|
@@ -29,7 +30,7 @@ import pump from 'pump';
|
|
29
30
|
// Compatible with old style formstream
|
30
31
|
import FormStream from 'formstream';
|
31
32
|
import { HttpAgent, CheckAddressFunction } from './HttpAgent';
|
32
|
-
import { RequestURL, RequestOptions, HttpMethod } from './Request';
|
33
|
+
import { RequestURL, RequestOptions, HttpMethod, RequestMeta } from './Request';
|
33
34
|
import { RawResponseWithMeta, HttpClientResponse, SocketInfo } from './Response';
|
34
35
|
import { parseJSON, sleep, digestAuthHeader, globalId, performanceTime, isReadable } from './utils';
|
35
36
|
import symbols from './symbols';
|
@@ -137,8 +138,26 @@ function defaultIsRetry(response: HttpClientResponse) {
|
|
137
138
|
|
138
139
|
type RequestContext = {
|
139
140
|
retries: number;
|
141
|
+
socketErrorRetries: number;
|
142
|
+
requestStartTime?: number;
|
140
143
|
};
|
141
144
|
|
145
|
+
const channels = {
|
146
|
+
request: diagnosticsChannel.channel('urllib:request'),
|
147
|
+
response: diagnosticsChannel.channel('urllib:response'),
|
148
|
+
};
|
149
|
+
|
150
|
+
export type RequestDiagnosticsMessage = {
|
151
|
+
request: RequestMeta;
|
152
|
+
};
|
153
|
+
|
154
|
+
export type ResponseDiagnosticsMessage = {
|
155
|
+
request: RequestMeta;
|
156
|
+
response: RawResponseWithMeta;
|
157
|
+
error?: Error;
|
158
|
+
};
|
159
|
+
|
160
|
+
|
142
161
|
export class HttpClient extends EventEmitter {
|
143
162
|
#defaultArgs?: RequestOptions;
|
144
163
|
#dispatcher?: Dispatcher;
|
@@ -188,6 +207,8 @@ export class HttpClient extends EventEmitter {
|
|
188
207
|
const headers: IncomingHttpHeaders = {};
|
189
208
|
const args = {
|
190
209
|
retry: 0,
|
210
|
+
socketErrorRetry: 1,
|
211
|
+
timing: true,
|
191
212
|
...this.#defaultArgs,
|
192
213
|
...options,
|
193
214
|
// keep method and headers exists on args for request event handler to easy use
|
@@ -196,9 +217,13 @@ export class HttpClient extends EventEmitter {
|
|
196
217
|
};
|
197
218
|
requestContext = {
|
198
219
|
retries: 0,
|
220
|
+
socketErrorRetries: 0,
|
199
221
|
...requestContext,
|
200
222
|
};
|
201
|
-
|
223
|
+
if (!requestContext.requestStartTime) {
|
224
|
+
requestContext.requestStartTime = performance.now();
|
225
|
+
}
|
226
|
+
const requestStartTime = requestContext.requestStartTime;
|
202
227
|
|
203
228
|
// https://developer.chrome.com/docs/devtools/network/reference/?utm_source=devtools#timing-explanation
|
204
229
|
const timing = {
|
@@ -232,8 +257,8 @@ export class HttpClient extends EventEmitter {
|
|
232
257
|
args,
|
233
258
|
ctx: args.ctx,
|
234
259
|
retries: requestContext.retries,
|
235
|
-
};
|
236
|
-
const socketInfo = {
|
260
|
+
} as RequestMeta;
|
261
|
+
const socketInfo: SocketInfo = {
|
237
262
|
id: 0,
|
238
263
|
localAddress: '',
|
239
264
|
localPort: 0,
|
@@ -259,6 +284,8 @@ export class HttpClient extends EventEmitter {
|
|
259
284
|
requestUrls: [],
|
260
285
|
timing,
|
261
286
|
socket: socketInfo,
|
287
|
+
retries: requestContext.retries,
|
288
|
+
socketErrorRetries: requestContext.socketErrorRetries,
|
262
289
|
} as any as RawResponseWithMeta;
|
263
290
|
|
264
291
|
let headersTimeout = 5000;
|
@@ -302,10 +329,19 @@ export class HttpClient extends EventEmitter {
|
|
302
329
|
if (requestContext.retries > 0) {
|
303
330
|
headers['x-urllib-retry'] = `${requestContext.retries}/${args.retry}`;
|
304
331
|
}
|
332
|
+
if (requestContext.socketErrorRetries > 0) {
|
333
|
+
headers['x-urllib-retry-on-socket-error'] = `${requestContext.socketErrorRetries}/${args.socketErrorRetry}`;
|
334
|
+
}
|
305
335
|
if (args.auth && !headers.authorization) {
|
306
336
|
headers.authorization = `Basic ${Buffer.from(args.auth).toString('base64')}`;
|
307
337
|
}
|
308
338
|
|
339
|
+
// streaming request should disable socketErrorRetry and retry
|
340
|
+
let isStreamingRequest = false;
|
341
|
+
if (args.dataType === 'stream' || args.writeStream) {
|
342
|
+
isStreamingRequest = true;
|
343
|
+
}
|
344
|
+
|
309
345
|
try {
|
310
346
|
const requestOptions: IUndiciRequestOption = {
|
311
347
|
method,
|
@@ -334,9 +370,11 @@ export class HttpClient extends EventEmitter {
|
|
334
370
|
if (isReadable(args.stream) && !(args.stream instanceof Readable)) {
|
335
371
|
debug('Request#%d convert old style stream to Readable', requestId);
|
336
372
|
args.stream = new Readable().wrap(args.stream);
|
373
|
+
isStreamingRequest = true;
|
337
374
|
} else if (args.stream instanceof FormStream) {
|
338
375
|
debug('Request#%d convert formstream to Readable', requestId);
|
339
376
|
args.stream = new Readable().wrap(args.stream);
|
377
|
+
isStreamingRequest = true;
|
340
378
|
}
|
341
379
|
args.content = args.stream;
|
342
380
|
}
|
@@ -380,6 +418,7 @@ export class HttpClient extends EventEmitter {
|
|
380
418
|
} else if (file instanceof Readable || isReadable(file as any)) {
|
381
419
|
const fileName = getFileName(file) || `streamfile${index}`;
|
382
420
|
formData.append(field, new BlobFromStream(file, mime.lookup(fileName) || ''), fileName);
|
421
|
+
isStreamingRequest = true;
|
383
422
|
}
|
384
423
|
}
|
385
424
|
|
@@ -403,6 +442,7 @@ export class HttpClient extends EventEmitter {
|
|
403
442
|
} else if (typeof args.content === 'string' && !headers['content-type']) {
|
404
443
|
headers['content-type'] = 'text/plain;charset=UTF-8';
|
405
444
|
}
|
445
|
+
isStreamingRequest = isReadable(args.content);
|
406
446
|
}
|
407
447
|
} else if (args.data) {
|
408
448
|
const isStringOrBufferOrReadable = typeof args.data === 'string'
|
@@ -419,6 +459,7 @@ export class HttpClient extends EventEmitter {
|
|
419
459
|
} else {
|
420
460
|
if (isStringOrBufferOrReadable) {
|
421
461
|
requestOptions.body = args.data;
|
462
|
+
isStreamingRequest = isReadable(args.data);
|
422
463
|
} else {
|
423
464
|
if (args.contentType === 'json'
|
424
465
|
|| args.contentType === 'application/json'
|
@@ -434,10 +475,17 @@ export class HttpClient extends EventEmitter {
|
|
434
475
|
}
|
435
476
|
}
|
436
477
|
}
|
478
|
+
if (isStreamingRequest) {
|
479
|
+
args.retry = 0;
|
480
|
+
args.socketErrorRetry = 0;
|
481
|
+
}
|
437
482
|
|
438
|
-
debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s',
|
439
|
-
requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout);
|
483
|
+
debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s, isStreamingRequest: %s',
|
484
|
+
requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout, isStreamingRequest);
|
440
485
|
requestOptions.headers = headers;
|
486
|
+
channels.request.publish({
|
487
|
+
request: reqMeta,
|
488
|
+
} as RequestDiagnosticsMessage);
|
441
489
|
if (this.listenerCount('request') > 0) {
|
442
490
|
this.emit('request', reqMeta);
|
443
491
|
}
|
@@ -486,8 +534,6 @@ export class HttpClient extends EventEmitter {
|
|
486
534
|
|
487
535
|
let data: any = null;
|
488
536
|
if (args.dataType === 'stream') {
|
489
|
-
// streaming mode will disable retry
|
490
|
-
args.retry = 0;
|
491
537
|
// only auto decompress on request args.compressed = true
|
492
538
|
if (args.compressed === true && isCompressedContent) {
|
493
539
|
// gzip or br
|
@@ -497,8 +543,6 @@ export class HttpClient extends EventEmitter {
|
|
497
543
|
res = Object.assign(response.body, res);
|
498
544
|
}
|
499
545
|
} else if (args.writeStream) {
|
500
|
-
// streaming mode will disable retry
|
501
|
-
args.retry = 0;
|
502
546
|
if (args.compressed === true && isCompressedContent) {
|
503
547
|
const decoder = contentEncoding === 'gzip' ? createGunzip() : createBrotliDecompress();
|
504
548
|
await pipelinePromise(response.body, decoder, args.writeStream);
|
@@ -556,6 +600,10 @@ export class HttpClient extends EventEmitter {
|
|
556
600
|
}
|
557
601
|
}
|
558
602
|
|
603
|
+
channels.response.publish({
|
604
|
+
request: reqMeta,
|
605
|
+
response: res,
|
606
|
+
} as ResponseDiagnosticsMessage);
|
559
607
|
if (this.listenerCount('response') > 0) {
|
560
608
|
this.emit('response', {
|
561
609
|
requestId,
|
@@ -577,11 +625,22 @@ export class HttpClient extends EventEmitter {
|
|
577
625
|
err = new HttpClientRequestTimeoutError(headersTimeout, { cause: e });
|
578
626
|
} else if (err.name === 'BodyTimeoutError') {
|
579
627
|
err = new HttpClientRequestTimeoutError(bodyTimeout, { cause: e });
|
628
|
+
} else if (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET') {
|
629
|
+
// auto retry on socket error, https://github.com/node-modules/urllib/issues/454
|
630
|
+
if (args.socketErrorRetry > 0 && requestContext.socketErrorRetries < args.socketErrorRetry) {
|
631
|
+
requestContext.socketErrorRetries++;
|
632
|
+
return await this.#requestInternal(url, options, requestContext);
|
633
|
+
}
|
580
634
|
}
|
581
635
|
err.opaque = orginalOpaque;
|
582
636
|
err.status = res.status;
|
583
637
|
err.headers = res.headers;
|
584
638
|
err.res = res;
|
639
|
+
if (err.socket) {
|
640
|
+
// store rawSocket
|
641
|
+
err._rawSocket = err.socket;
|
642
|
+
}
|
643
|
+
err.socket = socketInfo;
|
585
644
|
// make sure requestUrls not empty
|
586
645
|
if (res.requestUrls.length === 0) {
|
587
646
|
res.requestUrls.push(requestUrl.href);
|
@@ -589,6 +648,11 @@ export class HttpClient extends EventEmitter {
|
|
589
648
|
res.rt = performanceTime(requestStartTime);
|
590
649
|
this.#updateSocketInfo(socketInfo, internalOpaque);
|
591
650
|
|
651
|
+
channels.response.publish({
|
652
|
+
request: reqMeta,
|
653
|
+
response: res,
|
654
|
+
error: err,
|
655
|
+
} as ResponseDiagnosticsMessage);
|
592
656
|
if (this.listenerCount('response') > 0) {
|
593
657
|
this.emit('response', {
|
594
658
|
requestId,
|
@@ -611,13 +675,16 @@ export class HttpClient extends EventEmitter {
|
|
611
675
|
socketInfo.id = socket[symbols.kSocketId];
|
612
676
|
socketInfo.handledRequests = socket[symbols.kHandledRequests];
|
613
677
|
socketInfo.handledResponses = socket[symbols.kHandledResponses];
|
614
|
-
socketInfo.localAddress = socket.
|
615
|
-
socketInfo.localPort = socket.
|
678
|
+
socketInfo.localAddress = socket[symbols.kSocketLocalAddress];
|
679
|
+
socketInfo.localPort = socket[symbols.kSocketLocalPort];
|
616
680
|
socketInfo.remoteAddress = socket.remoteAddress;
|
617
681
|
socketInfo.remotePort = socket.remotePort;
|
618
682
|
socketInfo.remoteFamily = socket.remoteFamily;
|
619
683
|
socketInfo.bytesRead = socket.bytesRead;
|
620
684
|
socketInfo.bytesWritten = socket.bytesWritten;
|
685
|
+
socketInfo.connectedTime = socket[symbols.kSocketConnectedTime];
|
686
|
+
socketInfo.lastRequestEndTime = socket[symbols.kSocketRequestEndTime];
|
687
|
+
socket[symbols.kSocketRequestEndTime] = new Date();
|
621
688
|
}
|
622
689
|
}
|
623
690
|
}
|
package/src/Request.ts
CHANGED
@@ -69,11 +69,16 @@ export type RequestOptions = {
|
|
69
69
|
headers?: IncomingHttpHeaders;
|
70
70
|
/**
|
71
71
|
* Request timeout in milliseconds for connecting phase and response receiving phase.
|
72
|
-
* Defaults to
|
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
|
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
|
/**
|
@@ -134,3 +144,11 @@ export type RequestOptions = {
|
|
134
144
|
/** Default: `64 KiB` */
|
135
145
|
highWaterMark?: number;
|
136
146
|
};
|
147
|
+
|
148
|
+
export type RequestMeta = {
|
149
|
+
requestId: number;
|
150
|
+
url: string;
|
151
|
+
args: RequestOptions;
|
152
|
+
ctx?: unknown;
|
153
|
+
retries: number;
|
154
|
+
};
|
package/src/Response.ts
CHANGED
@@ -12,6 +12,8 @@ export type SocketInfo = {
|
|
12
12
|
bytesRead: number;
|
13
13
|
handledRequests: number;
|
14
14
|
handledResponses: number;
|
15
|
+
connectedTime?: Date;
|
16
|
+
lastRequestEndTime?: Date;
|
15
17
|
};
|
16
18
|
|
17
19
|
/**
|
@@ -47,6 +49,8 @@ export type RawResponseWithMeta = Readable & {
|
|
47
49
|
rt: number;
|
48
50
|
keepAliveSocket: boolean;
|
49
51
|
requestUrls: string[];
|
52
|
+
retries: number;
|
53
|
+
socketErrorRetries: number;
|
50
54
|
};
|
51
55
|
|
52
56
|
export type HttpClientResponse<T = any> = {
|
package/src/cjs/HttpClient.d.ts
CHANGED
@@ -4,8 +4,8 @@
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
5
5
|
import { LookupFunction } from 'node:net';
|
6
6
|
import { CheckAddressFunction } from './HttpAgent';
|
7
|
-
import { RequestURL, RequestOptions } from './Request';
|
8
|
-
import { HttpClientResponse } from './Response';
|
7
|
+
import { RequestURL, RequestOptions, RequestMeta } from './Request';
|
8
|
+
import { RawResponseWithMeta, HttpClientResponse } from './Response';
|
9
9
|
export type ClientOptions = {
|
10
10
|
defaultArgs?: RequestOptions;
|
11
11
|
/**
|
@@ -37,6 +37,14 @@ export type ClientOptions = {
|
|
37
37
|
};
|
38
38
|
};
|
39
39
|
export declare const HEADER_USER_AGENT: string;
|
40
|
+
export type RequestDiagnosticsMessage = {
|
41
|
+
request: RequestMeta;
|
42
|
+
};
|
43
|
+
export type ResponseDiagnosticsMessage = {
|
44
|
+
request: RequestMeta;
|
45
|
+
response: RawResponseWithMeta;
|
46
|
+
error?: Error;
|
47
|
+
};
|
40
48
|
export declare class HttpClient extends EventEmitter {
|
41
49
|
#private;
|
42
50
|
constructor(clientOptions?: ClientOptions);
|
package/src/cjs/HttpClient.js
CHANGED
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
4
|
};
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
6
6
|
exports.HttpClient = exports.HEADER_USER_AGENT = void 0;
|
7
|
+
const node_diagnostics_channel_1 = __importDefault(require("node:diagnostics_channel"));
|
7
8
|
const node_events_1 = require("node:events");
|
8
9
|
const node_http_1 = require("node:http");
|
9
10
|
const node_util_1 = require("node:util");
|
@@ -69,7 +70,7 @@ class HttpClientRequestTimeoutError extends Error {
|
|
69
70
|
Error.captureStackTrace(this, this.constructor);
|
70
71
|
}
|
71
72
|
}
|
72
|
-
exports.HEADER_USER_AGENT = (0, default_user_agent_1.default)('node-urllib', '3.
|
73
|
+
exports.HEADER_USER_AGENT = (0, default_user_agent_1.default)('node-urllib', '3.17.1');
|
73
74
|
function getFileName(stream) {
|
74
75
|
const filePath = stream.path;
|
75
76
|
if (filePath) {
|
@@ -80,6 +81,10 @@ function getFileName(stream) {
|
|
80
81
|
function defaultIsRetry(response) {
|
81
82
|
return response.status >= 500;
|
82
83
|
}
|
84
|
+
const channels = {
|
85
|
+
request: node_diagnostics_channel_1.default.channel('urllib:request'),
|
86
|
+
response: node_diagnostics_channel_1.default.channel('urllib:response'),
|
87
|
+
};
|
83
88
|
class HttpClient extends node_events_1.EventEmitter {
|
84
89
|
#defaultArgs;
|
85
90
|
#dispatcher;
|
@@ -126,6 +131,8 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
126
131
|
const headers = {};
|
127
132
|
const args = {
|
128
133
|
retry: 0,
|
134
|
+
socketErrorRetry: 1,
|
135
|
+
timing: true,
|
129
136
|
...this.#defaultArgs,
|
130
137
|
...options,
|
131
138
|
// keep method and headers exists on args for request event handler to easy use
|
@@ -134,9 +141,13 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
134
141
|
};
|
135
142
|
requestContext = {
|
136
143
|
retries: 0,
|
144
|
+
socketErrorRetries: 0,
|
137
145
|
...requestContext,
|
138
146
|
};
|
139
|
-
|
147
|
+
if (!requestContext.requestStartTime) {
|
148
|
+
requestContext.requestStartTime = node_perf_hooks_1.performance.now();
|
149
|
+
}
|
150
|
+
const requestStartTime = requestContext.requestStartTime;
|
140
151
|
// https://developer.chrome.com/docs/devtools/network/reference/?utm_source=devtools#timing-explanation
|
141
152
|
const timing = {
|
142
153
|
// socket assigned
|
@@ -196,6 +207,8 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
196
207
|
requestUrls: [],
|
197
208
|
timing,
|
198
209
|
socket: socketInfo,
|
210
|
+
retries: requestContext.retries,
|
211
|
+
socketErrorRetries: requestContext.socketErrorRetries,
|
199
212
|
};
|
200
213
|
let headersTimeout = 5000;
|
201
214
|
let bodyTimeout = 5000;
|
@@ -240,9 +253,17 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
240
253
|
if (requestContext.retries > 0) {
|
241
254
|
headers['x-urllib-retry'] = `${requestContext.retries}/${args.retry}`;
|
242
255
|
}
|
256
|
+
if (requestContext.socketErrorRetries > 0) {
|
257
|
+
headers['x-urllib-retry-on-socket-error'] = `${requestContext.socketErrorRetries}/${args.socketErrorRetry}`;
|
258
|
+
}
|
243
259
|
if (args.auth && !headers.authorization) {
|
244
260
|
headers.authorization = `Basic ${Buffer.from(args.auth).toString('base64')}`;
|
245
261
|
}
|
262
|
+
// streaming request should disable socketErrorRetry and retry
|
263
|
+
let isStreamingRequest = false;
|
264
|
+
if (args.dataType === 'stream' || args.writeStream) {
|
265
|
+
isStreamingRequest = true;
|
266
|
+
}
|
246
267
|
try {
|
247
268
|
const requestOptions = {
|
248
269
|
method,
|
@@ -270,10 +291,12 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
270
291
|
if ((0, utils_1.isReadable)(args.stream) && !(args.stream instanceof node_stream_1.Readable)) {
|
271
292
|
debug('Request#%d convert old style stream to Readable', requestId);
|
272
293
|
args.stream = new node_stream_1.Readable().wrap(args.stream);
|
294
|
+
isStreamingRequest = true;
|
273
295
|
}
|
274
296
|
else if (args.stream instanceof formstream_1.default) {
|
275
297
|
debug('Request#%d convert formstream to Readable', requestId);
|
276
298
|
args.stream = new node_stream_1.Readable().wrap(args.stream);
|
299
|
+
isStreamingRequest = true;
|
277
300
|
}
|
278
301
|
args.content = args.stream;
|
279
302
|
}
|
@@ -321,6 +344,7 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
321
344
|
else if (file instanceof node_stream_1.Readable || (0, utils_1.isReadable)(file)) {
|
322
345
|
const fileName = getFileName(file) || `streamfile${index}`;
|
323
346
|
formData.append(field, new BlobFromStream(file, mime_types_1.default.lookup(fileName) || ''), fileName);
|
347
|
+
isStreamingRequest = true;
|
324
348
|
}
|
325
349
|
}
|
326
350
|
if (undici_1.FormData) {
|
@@ -346,6 +370,7 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
346
370
|
else if (typeof args.content === 'string' && !headers['content-type']) {
|
347
371
|
headers['content-type'] = 'text/plain;charset=UTF-8';
|
348
372
|
}
|
373
|
+
isStreamingRequest = (0, utils_1.isReadable)(args.content);
|
349
374
|
}
|
350
375
|
}
|
351
376
|
else if (args.data) {
|
@@ -365,6 +390,7 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
365
390
|
else {
|
366
391
|
if (isStringOrBufferOrReadable) {
|
367
392
|
requestOptions.body = args.data;
|
393
|
+
isStreamingRequest = (0, utils_1.isReadable)(args.data);
|
368
394
|
}
|
369
395
|
else {
|
370
396
|
if (args.contentType === 'json'
|
@@ -382,8 +408,15 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
382
408
|
}
|
383
409
|
}
|
384
410
|
}
|
385
|
-
|
411
|
+
if (isStreamingRequest) {
|
412
|
+
args.retry = 0;
|
413
|
+
args.socketErrorRetry = 0;
|
414
|
+
}
|
415
|
+
debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s, isStreamingRequest: %s', requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout, isStreamingRequest);
|
386
416
|
requestOptions.headers = headers;
|
417
|
+
channels.request.publish({
|
418
|
+
request: reqMeta,
|
419
|
+
});
|
387
420
|
if (this.listenerCount('request') > 0) {
|
388
421
|
this.emit('request', reqMeta);
|
389
422
|
}
|
@@ -428,8 +461,6 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
428
461
|
}
|
429
462
|
let data = null;
|
430
463
|
if (args.dataType === 'stream') {
|
431
|
-
// streaming mode will disable retry
|
432
|
-
args.retry = 0;
|
433
464
|
// only auto decompress on request args.compressed = true
|
434
465
|
if (args.compressed === true && isCompressedContent) {
|
435
466
|
// gzip or br
|
@@ -441,8 +472,6 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
441
472
|
}
|
442
473
|
}
|
443
474
|
else if (args.writeStream) {
|
444
|
-
// streaming mode will disable retry
|
445
|
-
args.retry = 0;
|
446
475
|
if (args.compressed === true && isCompressedContent) {
|
447
476
|
const decoder = contentEncoding === 'gzip' ? (0, node_zlib_1.createGunzip)() : (0, node_zlib_1.createBrotliDecompress)();
|
448
477
|
await pipelinePromise(response.body, decoder, args.writeStream);
|
@@ -502,6 +531,10 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
502
531
|
return await this.#requestInternal(url, options, requestContext);
|
503
532
|
}
|
504
533
|
}
|
534
|
+
channels.response.publish({
|
535
|
+
request: reqMeta,
|
536
|
+
response: res,
|
537
|
+
});
|
505
538
|
if (this.listenerCount('response') > 0) {
|
506
539
|
this.emit('response', {
|
507
540
|
requestId,
|
@@ -525,16 +558,33 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
525
558
|
else if (err.name === 'BodyTimeoutError') {
|
526
559
|
err = new HttpClientRequestTimeoutError(bodyTimeout, { cause: e });
|
527
560
|
}
|
561
|
+
else if (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET') {
|
562
|
+
// auto retry on socket error, https://github.com/node-modules/urllib/issues/454
|
563
|
+
if (args.socketErrorRetry > 0 && requestContext.socketErrorRetries < args.socketErrorRetry) {
|
564
|
+
requestContext.socketErrorRetries++;
|
565
|
+
return await this.#requestInternal(url, options, requestContext);
|
566
|
+
}
|
567
|
+
}
|
528
568
|
err.opaque = orginalOpaque;
|
529
569
|
err.status = res.status;
|
530
570
|
err.headers = res.headers;
|
531
571
|
err.res = res;
|
572
|
+
if (err.socket) {
|
573
|
+
// store rawSocket
|
574
|
+
err._rawSocket = err.socket;
|
575
|
+
}
|
576
|
+
err.socket = socketInfo;
|
532
577
|
// make sure requestUrls not empty
|
533
578
|
if (res.requestUrls.length === 0) {
|
534
579
|
res.requestUrls.push(requestUrl.href);
|
535
580
|
}
|
536
581
|
res.rt = (0, utils_1.performanceTime)(requestStartTime);
|
537
582
|
this.#updateSocketInfo(socketInfo, internalOpaque);
|
583
|
+
channels.response.publish({
|
584
|
+
request: reqMeta,
|
585
|
+
response: res,
|
586
|
+
error: err,
|
587
|
+
});
|
538
588
|
if (this.listenerCount('response') > 0) {
|
539
589
|
this.emit('response', {
|
540
590
|
requestId,
|
@@ -556,13 +606,16 @@ class HttpClient extends node_events_1.EventEmitter {
|
|
556
606
|
socketInfo.id = socket[symbols_1.default.kSocketId];
|
557
607
|
socketInfo.handledRequests = socket[symbols_1.default.kHandledRequests];
|
558
608
|
socketInfo.handledResponses = socket[symbols_1.default.kHandledResponses];
|
559
|
-
socketInfo.localAddress = socket.
|
560
|
-
socketInfo.localPort = socket.
|
609
|
+
socketInfo.localAddress = socket[symbols_1.default.kSocketLocalAddress];
|
610
|
+
socketInfo.localPort = socket[symbols_1.default.kSocketLocalPort];
|
561
611
|
socketInfo.remoteAddress = socket.remoteAddress;
|
562
612
|
socketInfo.remotePort = socket.remotePort;
|
563
613
|
socketInfo.remoteFamily = socket.remoteFamily;
|
564
614
|
socketInfo.bytesRead = socket.bytesRead;
|
565
615
|
socketInfo.bytesWritten = socket.bytesWritten;
|
616
|
+
socketInfo.connectedTime = socket[symbols_1.default.kSocketConnectedTime];
|
617
|
+
socketInfo.lastRequestEndTime = socket[symbols_1.default.kSocketRequestEndTime];
|
618
|
+
socket[symbols_1.default.kSocketRequestEndTime] = new Date();
|
566
619
|
}
|
567
620
|
}
|
568
621
|
}
|