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/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
|
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
|
/**
|
@@ -132,3 +142,10 @@ export type RequestOptions = {
|
|
132
142
|
/** Default: `64 KiB` */
|
133
143
|
highWaterMark?: number;
|
134
144
|
};
|
145
|
+
export type RequestMeta = {
|
146
|
+
requestId: number;
|
147
|
+
url: string;
|
148
|
+
args: RequestOptions;
|
149
|
+
ctx?: unknown;
|
150
|
+
retries: number;
|
151
|
+
};
|
package/src/cjs/Response.d.ts
CHANGED
@@ -13,6 +13,8 @@ export type SocketInfo = {
|
|
13
13
|
bytesRead: number;
|
14
14
|
handledRequests: number;
|
15
15
|
handledResponses: number;
|
16
|
+
connectedTime?: Date;
|
17
|
+
lastRequestEndTime?: Date;
|
16
18
|
};
|
17
19
|
/**
|
18
20
|
* https://eggjs.org/en/core/httpclient.html#timing-boolean
|
@@ -37,6 +39,8 @@ export type RawResponseWithMeta = Readable & {
|
|
37
39
|
rt: number;
|
38
40
|
keepAliveSocket: boolean;
|
39
41
|
requestUrls: string[];
|
42
|
+
retries: number;
|
43
|
+
socketErrorRetries: number;
|
40
44
|
};
|
41
45
|
export type HttpClientResponse<T = any> = {
|
42
46
|
opaque: unknown;
|
@@ -21,6 +21,25 @@ let initedDiagnosticsChannel = false;
|
|
21
21
|
// server --> client
|
22
22
|
// undici:request:headers => { request, response }
|
23
23
|
// -> undici:request:trailers => { request, trailers }
|
24
|
+
function subscribe(name, listener) {
|
25
|
+
if (typeof node_diagnostics_channel_1.default.subscribe === 'function') {
|
26
|
+
node_diagnostics_channel_1.default.subscribe(name, listener);
|
27
|
+
}
|
28
|
+
else {
|
29
|
+
// TODO: support Node.js 14, will be removed on the next major version
|
30
|
+
node_diagnostics_channel_1.default.channel(name).subscribe(listener);
|
31
|
+
}
|
32
|
+
}
|
33
|
+
function formatSocket(socket) {
|
34
|
+
if (!socket)
|
35
|
+
return socket;
|
36
|
+
return {
|
37
|
+
localAddress: socket[symbols_1.default.kSocketLocalAddress],
|
38
|
+
localPort: socket[symbols_1.default.kSocketLocalPort],
|
39
|
+
remoteAddress: socket.remoteAddress,
|
40
|
+
remotePort: socket.remotePort,
|
41
|
+
};
|
42
|
+
}
|
24
43
|
function initDiagnosticsChannel() {
|
25
44
|
// makre sure init global DiagnosticsChannel once
|
26
45
|
if (initedDiagnosticsChannel)
|
@@ -29,7 +48,7 @@ function initDiagnosticsChannel() {
|
|
29
48
|
let kHandler;
|
30
49
|
// This message is published when a new outgoing request is created.
|
31
50
|
// Note: a request is only loosely completed to a given socket.
|
32
|
-
|
51
|
+
subscribe('undici:request:create', (message, name) => {
|
33
52
|
const { request } = message;
|
34
53
|
if (!kHandler) {
|
35
54
|
const symbols = Object.getOwnPropertySymbols(request);
|
@@ -52,16 +71,20 @@ function initDiagnosticsChannel() {
|
|
52
71
|
// diagnosticsChannel.channel('undici:client:beforeConnect')
|
53
72
|
// diagnosticsChannel.channel('undici:client:connectError')
|
54
73
|
// This message is published after a connection is established.
|
55
|
-
|
74
|
+
subscribe('undici:client:connected', (message, name) => {
|
56
75
|
const { socket } = message;
|
57
76
|
socket[symbols_1.default.kSocketId] = (0, utils_1.globalId)('UndiciSocket');
|
58
77
|
socket[symbols_1.default.kSocketStartTime] = node_perf_hooks_1.performance.now();
|
78
|
+
socket[symbols_1.default.kSocketConnectedTime] = new Date();
|
59
79
|
socket[symbols_1.default.kHandledRequests] = 0;
|
60
80
|
socket[symbols_1.default.kHandledResponses] = 0;
|
61
|
-
|
81
|
+
// copy local address to symbol, avoid them be reset after request error throw
|
82
|
+
socket[symbols_1.default.kSocketLocalAddress] = socket.localAddress;
|
83
|
+
socket[symbols_1.default.kSocketLocalPort] = socket.localPort;
|
84
|
+
debug('[%s] Socket#%d connected (sock: %o)', name, socket[symbols_1.default.kSocketId], formatSocket(socket));
|
62
85
|
});
|
63
86
|
// This message is published right before the first byte of the request is written to the socket.
|
64
|
-
|
87
|
+
subscribe('undici:client:sendHeaders', (message, name) => {
|
65
88
|
const { request, socket } = message;
|
66
89
|
if (!kHandler)
|
67
90
|
return;
|
@@ -71,7 +94,7 @@ function initDiagnosticsChannel() {
|
|
71
94
|
socket[symbols_1.default.kHandledRequests]++;
|
72
95
|
// attach socket to opaque
|
73
96
|
opaque[symbols_1.default.kRequestSocket] = socket;
|
74
|
-
debug('[%s] Request#%d send headers on Socket#%d (handled %d requests)', name, opaque[symbols_1.default.kRequestId], socket[symbols_1.default.kSocketId], socket[symbols_1.default.kHandledRequests]);
|
97
|
+
debug('[%s] Request#%d send headers on Socket#%d (handled %d requests, sock: %o)', name, opaque[symbols_1.default.kRequestId], socket[symbols_1.default.kSocketId], socket[symbols_1.default.kHandledRequests], formatSocket(socket));
|
75
98
|
if (!opaque[symbols_1.default.kEnableRequestTiming])
|
76
99
|
return;
|
77
100
|
opaque[symbols_1.default.kRequestTiming].requestHeadersSent = (0, utils_1.performanceTime)(opaque[symbols_1.default.kRequestStartTime]);
|
@@ -82,7 +105,7 @@ function initDiagnosticsChannel() {
|
|
82
105
|
(0, utils_1.performanceTime)(opaque[symbols_1.default.kRequestStartTime], socket[symbols_1.default.kSocketStartTime]);
|
83
106
|
}
|
84
107
|
});
|
85
|
-
|
108
|
+
subscribe('undici:request:bodySent', (message, name) => {
|
86
109
|
const { request } = message;
|
87
110
|
if (!kHandler)
|
88
111
|
return;
|
@@ -95,7 +118,7 @@ function initDiagnosticsChannel() {
|
|
95
118
|
opaque[symbols_1.default.kRequestTiming].requestSent = (0, utils_1.performanceTime)(opaque[symbols_1.default.kRequestStartTime]);
|
96
119
|
});
|
97
120
|
// This message is published after the response headers have been received, i.e. the response has been completed.
|
98
|
-
|
121
|
+
subscribe('undici:request:headers', (message, name) => {
|
99
122
|
const { request, response } = message;
|
100
123
|
if (!kHandler)
|
101
124
|
return;
|
@@ -105,13 +128,13 @@ function initDiagnosticsChannel() {
|
|
105
128
|
// get socket from opaque
|
106
129
|
const socket = opaque[symbols_1.default.kRequestSocket];
|
107
130
|
socket[symbols_1.default.kHandledResponses]++;
|
108
|
-
debug('[%s] Request#%d get %s response headers on Socket#%d (handled %d responses)', name, opaque[symbols_1.default.kRequestId], response.statusCode, socket[symbols_1.default.kSocketId], socket[symbols_1.default.kHandledResponses]);
|
131
|
+
debug('[%s] Request#%d get %s response headers on Socket#%d (handled %d responses, sock: %o)', name, opaque[symbols_1.default.kRequestId], response.statusCode, socket[symbols_1.default.kSocketId], socket[symbols_1.default.kHandledResponses], formatSocket(socket));
|
109
132
|
if (!opaque[symbols_1.default.kEnableRequestTiming])
|
110
133
|
return;
|
111
134
|
opaque[symbols_1.default.kRequestTiming].waiting = (0, utils_1.performanceTime)(opaque[symbols_1.default.kRequestStartTime]);
|
112
135
|
});
|
113
136
|
// This message is published after the response body and trailers have been received, i.e. the response has been completed.
|
114
|
-
|
137
|
+
subscribe('undici:request:trailers', (message, name) => {
|
115
138
|
const { request } = message;
|
116
139
|
if (!kHandler)
|
117
140
|
return;
|
@@ -123,6 +146,15 @@ function initDiagnosticsChannel() {
|
|
123
146
|
return;
|
124
147
|
opaque[symbols_1.default.kRequestTiming].contentDownload = (0, utils_1.performanceTime)(opaque[symbols_1.default.kRequestStartTime]);
|
125
148
|
});
|
126
|
-
//
|
149
|
+
// This message is published if the request is going to error, but it has not errored yet.
|
150
|
+
// subscribe('undici:request:error', (message, name) => {
|
151
|
+
// const { request, error } = message as DiagnosticsChannel.RequestErrorMessage;
|
152
|
+
// const opaque = request[kHandler]?.opts?.opaque;
|
153
|
+
// if (!opaque || !opaque[symbols.kRequestId]) return;
|
154
|
+
// const socket = opaque[symbols.kRequestSocket];
|
155
|
+
// debug('[%s] Request#%d error on Socket#%d (handled %d responses, sock: %o), error: %o',
|
156
|
+
// name, opaque[symbols.kRequestId], socket[symbols.kSocketId], socket[symbols.kHandledResponses],
|
157
|
+
// formatSocket(socket), error);
|
158
|
+
// });
|
127
159
|
}
|
128
160
|
exports.initDiagnosticsChannel = initDiagnosticsChannel;
|
package/src/cjs/index.d.ts
CHANGED
@@ -2,7 +2,7 @@ import { RequestOptions, RequestURL } from './Request';
|
|
2
2
|
export declare function request<T = any>(url: RequestURL, options?: RequestOptions): Promise<import("./Response").HttpClientResponse<T>>;
|
3
3
|
export declare function curl<T = any>(url: RequestURL, options?: RequestOptions): Promise<import("./Response").HttpClientResponse<T>>;
|
4
4
|
export { MockAgent, ProxyAgent, Agent, Dispatcher, setGlobalDispatcher, getGlobalDispatcher, } from 'undici';
|
5
|
-
export { HttpClient, HttpClient as HttpClient2, HEADER_USER_AGENT as USER_AGENT } from './HttpClient';
|
5
|
+
export { HttpClient, HttpClient as HttpClient2, HEADER_USER_AGENT as USER_AGENT, RequestDiagnosticsMessage, ResponseDiagnosticsMessage, } from './HttpClient';
|
6
6
|
export { RequestOptions, RequestOptions as RequestOptions2, RequestURL, HttpMethod, FixJSONCtlCharsHandler, FixJSONCtlChars, } from './Request';
|
7
7
|
export { SocketInfo, Timing, RawResponseWithMeta, HttpClientResponse } from './Response';
|
8
8
|
declare const _default: {
|
package/src/cjs/symbols.d.ts
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
declare const _default: {
|
2
2
|
kSocketId: symbol;
|
3
3
|
kSocketStartTime: symbol;
|
4
|
+
kSocketConnectedTime: symbol;
|
5
|
+
kSocketRequestEndTime: symbol;
|
6
|
+
kSocketLocalAddress: symbol;
|
7
|
+
kSocketLocalPort: symbol;
|
4
8
|
kHandledRequests: symbol;
|
5
9
|
kHandledResponses: symbol;
|
6
10
|
kRequestSocket: symbol;
|
package/src/cjs/symbols.js
CHANGED
@@ -3,6 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.default = {
|
4
4
|
kSocketId: Symbol('socket id'),
|
5
5
|
kSocketStartTime: Symbol('socket start time'),
|
6
|
+
kSocketConnectedTime: Symbol('socket connected time'),
|
7
|
+
kSocketRequestEndTime: Symbol('socket request end time'),
|
8
|
+
kSocketLocalAddress: Symbol('socket local address'),
|
9
|
+
kSocketLocalPort: Symbol('socket local port'),
|
6
10
|
kHandledRequests: Symbol('handled requests per socket'),
|
7
11
|
kHandledResponses: Symbol('handled responses per socket'),
|
8
12
|
kRequestSocket: Symbol('request on the socket'),
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import diagnosticsChannel from 'node:diagnostics_channel';
|
2
2
|
import { performance } from 'node:perf_hooks';
|
3
3
|
import { debuglog } from 'node:util';
|
4
|
+
import { Socket } from 'node:net';
|
4
5
|
import { DiagnosticsChannel } from 'undici';
|
5
6
|
import symbols from './symbols';
|
6
7
|
import { globalId, performanceTime } from './utils';
|
@@ -17,6 +18,26 @@ let initedDiagnosticsChannel = false;
|
|
17
18
|
// server --> client
|
18
19
|
// undici:request:headers => { request, response }
|
19
20
|
// -> undici:request:trailers => { request, trailers }
|
21
|
+
|
22
|
+
function subscribe(name: string, listener: (message: unknown, channelName: string | symbol) => void) {
|
23
|
+
if (typeof diagnosticsChannel.subscribe === 'function') {
|
24
|
+
diagnosticsChannel.subscribe(name, listener);
|
25
|
+
} else {
|
26
|
+
// TODO: support Node.js 14, will be removed on the next major version
|
27
|
+
diagnosticsChannel.channel(name).subscribe(listener);
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
function formatSocket(socket: Socket) {
|
32
|
+
if (!socket) return socket;
|
33
|
+
return {
|
34
|
+
localAddress: socket[symbols.kSocketLocalAddress],
|
35
|
+
localPort: socket[symbols.kSocketLocalPort],
|
36
|
+
remoteAddress: socket.remoteAddress,
|
37
|
+
remotePort: socket.remotePort,
|
38
|
+
};
|
39
|
+
}
|
40
|
+
|
20
41
|
export function initDiagnosticsChannel() {
|
21
42
|
// makre sure init global DiagnosticsChannel once
|
22
43
|
if (initedDiagnosticsChannel) return;
|
@@ -25,7 +46,7 @@ export function initDiagnosticsChannel() {
|
|
25
46
|
let kHandler: symbol;
|
26
47
|
// This message is published when a new outgoing request is created.
|
27
48
|
// Note: a request is only loosely completed to a given socket.
|
28
|
-
|
49
|
+
subscribe('undici:request:create', (message, name) => {
|
29
50
|
const { request } = message as DiagnosticsChannel.RequestCreateMessage;
|
30
51
|
if (!kHandler) {
|
31
52
|
const symbols = Object.getOwnPropertySymbols(request);
|
@@ -48,17 +69,21 @@ export function initDiagnosticsChannel() {
|
|
48
69
|
// diagnosticsChannel.channel('undici:client:beforeConnect')
|
49
70
|
// diagnosticsChannel.channel('undici:client:connectError')
|
50
71
|
// This message is published after a connection is established.
|
51
|
-
|
72
|
+
subscribe('undici:client:connected', (message, name) => {
|
52
73
|
const { socket } = message as DiagnosticsChannel.ClientConnectedMessage;
|
53
74
|
socket[symbols.kSocketId] = globalId('UndiciSocket');
|
54
75
|
socket[symbols.kSocketStartTime] = performance.now();
|
76
|
+
socket[symbols.kSocketConnectedTime] = new Date();
|
55
77
|
socket[symbols.kHandledRequests] = 0;
|
56
78
|
socket[symbols.kHandledResponses] = 0;
|
57
|
-
|
79
|
+
// copy local address to symbol, avoid them be reset after request error throw
|
80
|
+
socket[symbols.kSocketLocalAddress] = socket.localAddress;
|
81
|
+
socket[symbols.kSocketLocalPort] = socket.localPort;
|
82
|
+
debug('[%s] Socket#%d connected (sock: %o)', name, socket[symbols.kSocketId], formatSocket(socket));
|
58
83
|
});
|
59
84
|
|
60
85
|
// This message is published right before the first byte of the request is written to the socket.
|
61
|
-
|
86
|
+
subscribe('undici:client:sendHeaders', (message, name) => {
|
62
87
|
const { request, socket } = message as DiagnosticsChannel.ClientSendHeadersMessage;
|
63
88
|
if (!kHandler) return;
|
64
89
|
const opaque = request[kHandler]?.opts?.opaque;
|
@@ -67,8 +92,9 @@ export function initDiagnosticsChannel() {
|
|
67
92
|
socket[symbols.kHandledRequests]++;
|
68
93
|
// attach socket to opaque
|
69
94
|
opaque[symbols.kRequestSocket] = socket;
|
70
|
-
debug('[%s] Request#%d send headers on Socket#%d (handled %d requests)',
|
71
|
-
name, opaque[symbols.kRequestId], socket[symbols.kSocketId], socket[symbols.kHandledRequests]
|
95
|
+
debug('[%s] Request#%d send headers on Socket#%d (handled %d requests, sock: %o)',
|
96
|
+
name, opaque[symbols.kRequestId], socket[symbols.kSocketId], socket[symbols.kHandledRequests],
|
97
|
+
formatSocket(socket));
|
72
98
|
|
73
99
|
if (!opaque[symbols.kEnableRequestTiming]) return;
|
74
100
|
opaque[symbols.kRequestTiming].requestHeadersSent = performanceTime(opaque[symbols.kRequestStartTime]);
|
@@ -80,7 +106,7 @@ export function initDiagnosticsChannel() {
|
|
80
106
|
}
|
81
107
|
});
|
82
108
|
|
83
|
-
|
109
|
+
subscribe('undici:request:bodySent', (message, name) => {
|
84
110
|
const { request } = message as DiagnosticsChannel.RequestBodySentMessage;
|
85
111
|
if (!kHandler) return;
|
86
112
|
const opaque = request[kHandler]?.opts?.opaque;
|
@@ -92,7 +118,7 @@ export function initDiagnosticsChannel() {
|
|
92
118
|
});
|
93
119
|
|
94
120
|
// This message is published after the response headers have been received, i.e. the response has been completed.
|
95
|
-
|
121
|
+
subscribe('undici:request:headers', (message, name) => {
|
96
122
|
const { request, response } = message as DiagnosticsChannel.RequestHeadersMessage;
|
97
123
|
if (!kHandler) return;
|
98
124
|
const opaque = request[kHandler]?.opts?.opaque;
|
@@ -101,15 +127,16 @@ export function initDiagnosticsChannel() {
|
|
101
127
|
// get socket from opaque
|
102
128
|
const socket = opaque[symbols.kRequestSocket];
|
103
129
|
socket[symbols.kHandledResponses]++;
|
104
|
-
debug('[%s] Request#%d get %s response headers on Socket#%d (handled %d responses)',
|
105
|
-
name, opaque[symbols.kRequestId], response.statusCode, socket[symbols.kSocketId], socket[symbols.kHandledResponses]
|
130
|
+
debug('[%s] Request#%d get %s response headers on Socket#%d (handled %d responses, sock: %o)',
|
131
|
+
name, opaque[symbols.kRequestId], response.statusCode, socket[symbols.kSocketId], socket[symbols.kHandledResponses],
|
132
|
+
formatSocket(socket));
|
106
133
|
|
107
134
|
if (!opaque[symbols.kEnableRequestTiming]) return;
|
108
135
|
opaque[symbols.kRequestTiming].waiting = performanceTime(opaque[symbols.kRequestStartTime]);
|
109
136
|
});
|
110
137
|
|
111
138
|
// This message is published after the response body and trailers have been received, i.e. the response has been completed.
|
112
|
-
|
139
|
+
subscribe('undici:request:trailers', (message, name) => {
|
113
140
|
const { request } = message as DiagnosticsChannel.RequestTrailersMessage;
|
114
141
|
if (!kHandler) return;
|
115
142
|
const opaque = request[kHandler]?.opts?.opaque;
|
@@ -120,5 +147,15 @@ export function initDiagnosticsChannel() {
|
|
120
147
|
if (!opaque[symbols.kEnableRequestTiming]) return;
|
121
148
|
opaque[symbols.kRequestTiming].contentDownload = performanceTime(opaque[symbols.kRequestStartTime]);
|
122
149
|
});
|
123
|
-
|
150
|
+
|
151
|
+
// This message is published if the request is going to error, but it has not errored yet.
|
152
|
+
// subscribe('undici:request:error', (message, name) => {
|
153
|
+
// const { request, error } = message as DiagnosticsChannel.RequestErrorMessage;
|
154
|
+
// const opaque = request[kHandler]?.opts?.opaque;
|
155
|
+
// if (!opaque || !opaque[symbols.kRequestId]) return;
|
156
|
+
// const socket = opaque[symbols.kRequestSocket];
|
157
|
+
// debug('[%s] Request#%d error on Socket#%d (handled %d responses, sock: %o), error: %o',
|
158
|
+
// name, opaque[symbols.kRequestId], socket[symbols.kSocketId], socket[symbols.kHandledResponses],
|
159
|
+
// formatSocket(socket), error);
|
160
|
+
// });
|
124
161
|
}
|
package/src/esm/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/esm/HttpClient.js
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import diagnosticsChannel from 'node:diagnostics_channel';
|
1
2
|
import { EventEmitter } from 'node:events';
|
2
3
|
import { STATUS_CODES } from 'node:http';
|
3
4
|
import { debuglog } from 'node:util';
|
@@ -63,7 +64,7 @@ class HttpClientRequestTimeoutError extends Error {
|
|
63
64
|
Error.captureStackTrace(this, this.constructor);
|
64
65
|
}
|
65
66
|
}
|
66
|
-
export const HEADER_USER_AGENT = createUserAgent('node-urllib', '3.
|
67
|
+
export const HEADER_USER_AGENT = createUserAgent('node-urllib', '3.17.1');
|
67
68
|
function getFileName(stream) {
|
68
69
|
const filePath = stream.path;
|
69
70
|
if (filePath) {
|
@@ -74,6 +75,10 @@ function getFileName(stream) {
|
|
74
75
|
function defaultIsRetry(response) {
|
75
76
|
return response.status >= 500;
|
76
77
|
}
|
78
|
+
const channels = {
|
79
|
+
request: diagnosticsChannel.channel('urllib:request'),
|
80
|
+
response: diagnosticsChannel.channel('urllib:response'),
|
81
|
+
};
|
77
82
|
export class HttpClient extends EventEmitter {
|
78
83
|
#defaultArgs;
|
79
84
|
#dispatcher;
|
@@ -120,6 +125,8 @@ export class HttpClient extends EventEmitter {
|
|
120
125
|
const headers = {};
|
121
126
|
const args = {
|
122
127
|
retry: 0,
|
128
|
+
socketErrorRetry: 1,
|
129
|
+
timing: true,
|
123
130
|
...this.#defaultArgs,
|
124
131
|
...options,
|
125
132
|
// keep method and headers exists on args for request event handler to easy use
|
@@ -128,9 +135,13 @@ export class HttpClient extends EventEmitter {
|
|
128
135
|
};
|
129
136
|
requestContext = {
|
130
137
|
retries: 0,
|
138
|
+
socketErrorRetries: 0,
|
131
139
|
...requestContext,
|
132
140
|
};
|
133
|
-
|
141
|
+
if (!requestContext.requestStartTime) {
|
142
|
+
requestContext.requestStartTime = performance.now();
|
143
|
+
}
|
144
|
+
const requestStartTime = requestContext.requestStartTime;
|
134
145
|
// https://developer.chrome.com/docs/devtools/network/reference/?utm_source=devtools#timing-explanation
|
135
146
|
const timing = {
|
136
147
|
// socket assigned
|
@@ -190,6 +201,8 @@ export class HttpClient extends EventEmitter {
|
|
190
201
|
requestUrls: [],
|
191
202
|
timing,
|
192
203
|
socket: socketInfo,
|
204
|
+
retries: requestContext.retries,
|
205
|
+
socketErrorRetries: requestContext.socketErrorRetries,
|
193
206
|
};
|
194
207
|
let headersTimeout = 5000;
|
195
208
|
let bodyTimeout = 5000;
|
@@ -234,9 +247,17 @@ export class HttpClient extends EventEmitter {
|
|
234
247
|
if (requestContext.retries > 0) {
|
235
248
|
headers['x-urllib-retry'] = `${requestContext.retries}/${args.retry}`;
|
236
249
|
}
|
250
|
+
if (requestContext.socketErrorRetries > 0) {
|
251
|
+
headers['x-urllib-retry-on-socket-error'] = `${requestContext.socketErrorRetries}/${args.socketErrorRetry}`;
|
252
|
+
}
|
237
253
|
if (args.auth && !headers.authorization) {
|
238
254
|
headers.authorization = `Basic ${Buffer.from(args.auth).toString('base64')}`;
|
239
255
|
}
|
256
|
+
// streaming request should disable socketErrorRetry and retry
|
257
|
+
let isStreamingRequest = false;
|
258
|
+
if (args.dataType === 'stream' || args.writeStream) {
|
259
|
+
isStreamingRequest = true;
|
260
|
+
}
|
240
261
|
try {
|
241
262
|
const requestOptions = {
|
242
263
|
method,
|
@@ -264,10 +285,12 @@ export class HttpClient extends EventEmitter {
|
|
264
285
|
if (isReadable(args.stream) && !(args.stream instanceof Readable)) {
|
265
286
|
debug('Request#%d convert old style stream to Readable', requestId);
|
266
287
|
args.stream = new Readable().wrap(args.stream);
|
288
|
+
isStreamingRequest = true;
|
267
289
|
}
|
268
290
|
else if (args.stream instanceof FormStream) {
|
269
291
|
debug('Request#%d convert formstream to Readable', requestId);
|
270
292
|
args.stream = new Readable().wrap(args.stream);
|
293
|
+
isStreamingRequest = true;
|
271
294
|
}
|
272
295
|
args.content = args.stream;
|
273
296
|
}
|
@@ -315,6 +338,7 @@ export class HttpClient extends EventEmitter {
|
|
315
338
|
else if (file instanceof Readable || isReadable(file)) {
|
316
339
|
const fileName = getFileName(file) || `streamfile${index}`;
|
317
340
|
formData.append(field, new BlobFromStream(file, mime.lookup(fileName) || ''), fileName);
|
341
|
+
isStreamingRequest = true;
|
318
342
|
}
|
319
343
|
}
|
320
344
|
if (FormDataNative) {
|
@@ -340,6 +364,7 @@ export class HttpClient extends EventEmitter {
|
|
340
364
|
else if (typeof args.content === 'string' && !headers['content-type']) {
|
341
365
|
headers['content-type'] = 'text/plain;charset=UTF-8';
|
342
366
|
}
|
367
|
+
isStreamingRequest = isReadable(args.content);
|
343
368
|
}
|
344
369
|
}
|
345
370
|
else if (args.data) {
|
@@ -359,6 +384,7 @@ export class HttpClient extends EventEmitter {
|
|
359
384
|
else {
|
360
385
|
if (isStringOrBufferOrReadable) {
|
361
386
|
requestOptions.body = args.data;
|
387
|
+
isStreamingRequest = isReadable(args.data);
|
362
388
|
}
|
363
389
|
else {
|
364
390
|
if (args.contentType === 'json'
|
@@ -376,8 +402,15 @@ export class HttpClient extends EventEmitter {
|
|
376
402
|
}
|
377
403
|
}
|
378
404
|
}
|
379
|
-
|
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);
|
380
410
|
requestOptions.headers = headers;
|
411
|
+
channels.request.publish({
|
412
|
+
request: reqMeta,
|
413
|
+
});
|
381
414
|
if (this.listenerCount('request') > 0) {
|
382
415
|
this.emit('request', reqMeta);
|
383
416
|
}
|
@@ -422,8 +455,6 @@ export class HttpClient extends EventEmitter {
|
|
422
455
|
}
|
423
456
|
let data = null;
|
424
457
|
if (args.dataType === 'stream') {
|
425
|
-
// streaming mode will disable retry
|
426
|
-
args.retry = 0;
|
427
458
|
// only auto decompress on request args.compressed = true
|
428
459
|
if (args.compressed === true && isCompressedContent) {
|
429
460
|
// gzip or br
|
@@ -435,8 +466,6 @@ export class HttpClient extends EventEmitter {
|
|
435
466
|
}
|
436
467
|
}
|
437
468
|
else if (args.writeStream) {
|
438
|
-
// streaming mode will disable retry
|
439
|
-
args.retry = 0;
|
440
469
|
if (args.compressed === true && isCompressedContent) {
|
441
470
|
const decoder = contentEncoding === 'gzip' ? createGunzip() : createBrotliDecompress();
|
442
471
|
await pipelinePromise(response.body, decoder, args.writeStream);
|
@@ -496,6 +525,10 @@ export class HttpClient extends EventEmitter {
|
|
496
525
|
return await this.#requestInternal(url, options, requestContext);
|
497
526
|
}
|
498
527
|
}
|
528
|
+
channels.response.publish({
|
529
|
+
request: reqMeta,
|
530
|
+
response: res,
|
531
|
+
});
|
499
532
|
if (this.listenerCount('response') > 0) {
|
500
533
|
this.emit('response', {
|
501
534
|
requestId,
|
@@ -519,16 +552,33 @@ export class HttpClient extends EventEmitter {
|
|
519
552
|
else if (err.name === 'BodyTimeoutError') {
|
520
553
|
err = new HttpClientRequestTimeoutError(bodyTimeout, { cause: e });
|
521
554
|
}
|
555
|
+
else if (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET') {
|
556
|
+
// auto retry on socket error, https://github.com/node-modules/urllib/issues/454
|
557
|
+
if (args.socketErrorRetry > 0 && requestContext.socketErrorRetries < args.socketErrorRetry) {
|
558
|
+
requestContext.socketErrorRetries++;
|
559
|
+
return await this.#requestInternal(url, options, requestContext);
|
560
|
+
}
|
561
|
+
}
|
522
562
|
err.opaque = orginalOpaque;
|
523
563
|
err.status = res.status;
|
524
564
|
err.headers = res.headers;
|
525
565
|
err.res = res;
|
566
|
+
if (err.socket) {
|
567
|
+
// store rawSocket
|
568
|
+
err._rawSocket = err.socket;
|
569
|
+
}
|
570
|
+
err.socket = socketInfo;
|
526
571
|
// make sure requestUrls not empty
|
527
572
|
if (res.requestUrls.length === 0) {
|
528
573
|
res.requestUrls.push(requestUrl.href);
|
529
574
|
}
|
530
575
|
res.rt = performanceTime(requestStartTime);
|
531
576
|
this.#updateSocketInfo(socketInfo, internalOpaque);
|
577
|
+
channels.response.publish({
|
578
|
+
request: reqMeta,
|
579
|
+
response: res,
|
580
|
+
error: err,
|
581
|
+
});
|
532
582
|
if (this.listenerCount('response') > 0) {
|
533
583
|
this.emit('response', {
|
534
584
|
requestId,
|
@@ -550,13 +600,16 @@ export class HttpClient extends EventEmitter {
|
|
550
600
|
socketInfo.id = socket[symbols.kSocketId];
|
551
601
|
socketInfo.handledRequests = socket[symbols.kHandledRequests];
|
552
602
|
socketInfo.handledResponses = socket[symbols.kHandledResponses];
|
553
|
-
socketInfo.localAddress = socket.
|
554
|
-
socketInfo.localPort = socket.
|
603
|
+
socketInfo.localAddress = socket[symbols.kSocketLocalAddress];
|
604
|
+
socketInfo.localPort = socket[symbols.kSocketLocalPort];
|
555
605
|
socketInfo.remoteAddress = socket.remoteAddress;
|
556
606
|
socketInfo.remotePort = socket.remotePort;
|
557
607
|
socketInfo.remoteFamily = socket.remoteFamily;
|
558
608
|
socketInfo.bytesRead = socket.bytesRead;
|
559
609
|
socketInfo.bytesWritten = socket.bytesWritten;
|
610
|
+
socketInfo.connectedTime = socket[symbols.kSocketConnectedTime];
|
611
|
+
socketInfo.lastRequestEndTime = socket[symbols.kSocketRequestEndTime];
|
612
|
+
socket[symbols.kSocketRequestEndTime] = new Date();
|
560
613
|
}
|
561
614
|
}
|
562
615
|
}
|