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.
@@ -67,11 +67,16 @@ export type RequestOptions = {
67
67
  headers?: IncomingHttpHeaders;
68
68
  /**
69
69
  * Request timeout in milliseconds for connecting phase and response receiving phase.
70
- * Defaults to exports.
71
- * TIMEOUT, both are 5s. You can use timeout: 5000 to tell urllib use same timeout on two phase or set them seperately such as
70
+ * Defaults is `5000`, both are 5 seconds. You can use timeout: 5000 to tell urllib use same timeout on two phase or set them separately such as
72
71
  * timeout: [3000, 5000], which will set connecting timeout to 3s and response 5s.
73
72
  */
74
73
  timeout?: number | number[];
74
+ /**
75
+ * Default is `4000`, 4 seconds - The timeout after which a socket without active requests will time out.
76
+ * Monitors time between activity on a connected socket.
77
+ * This value may be overridden by *keep-alive* hints from the server. See [MDN: HTTP - Headers - Keep-Alive directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive#directives) for more details.
78
+ */
79
+ keepAliveTimeout?: number;
75
80
  /**
76
81
  * username:password used in HTTP Basic Authorization.
77
82
  * Alias to `headers.authorization = xxx`
@@ -89,7 +94,7 @@ export type RequestOptions = {
89
94
  formatRedirectUrl?: (a: any, b: any) => void;
90
95
  /** Before request hook, you can change every thing here. */
91
96
  beforeRequest?: (...args: any[]) => void;
92
- /** Accept `gzip, br` response content and auto decode it, default is false. */
97
+ /** Accept `gzip, br` response content and auto decode it, default is `true`. */
93
98
  compressed?: boolean;
94
99
  /**
95
100
  * @deprecated
@@ -97,11 +102,11 @@ export type RequestOptions = {
97
102
  * */
98
103
  gzip?: boolean;
99
104
  /**
100
- * Enable timing or not, default is false.
105
+ * Enable timing or not, default is `true`.
101
106
  * */
102
107
  timing?: boolean;
103
108
  /**
104
- * Auto retry times on 5xx response, default is 0. Don't work on streaming request
109
+ * Auto retry times on 5xx response, default is `0`. Don't work on streaming request
105
110
  * It's not supported by using retry and writeStream, because the retry request can't stop the stream which is consuming.
106
111
  **/
107
112
  retry?: number;
@@ -112,6 +117,11 @@ export type RequestOptions = {
112
117
  * It will retry when status >= 500 by default. Request error is not included.
113
118
  */
114
119
  isRetry?: (response: HttpClientResponse) => boolean;
120
+ /**
121
+ * Auto retry times on socket error, default is `1`. Don't work on streaming request
122
+ * It's not supported by using retry and writeStream, because the retry request can't stop the stream which is consuming.
123
+ **/
124
+ socketErrorRetry?: number;
115
125
  /** Default: `null` */
116
126
  opaque?: unknown;
117
127
  /**
@@ -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
+ };
@@ -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
- node_diagnostics_channel_1.default.channel('undici:request:create').subscribe((message, name) => {
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
- node_diagnostics_channel_1.default.channel('undici:client:connected').subscribe((message, name) => {
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
- debug('[%s] Socket#%d connected', name, socket[symbols_1.default.kSocketId]);
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
- node_diagnostics_channel_1.default.channel('undici:client:sendHeaders').subscribe((message, name) => {
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
- node_diagnostics_channel_1.default.channel('undici:request:bodySent').subscribe((message, name) => {
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
- node_diagnostics_channel_1.default.channel('undici:request:headers').subscribe((message, name) => {
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
- node_diagnostics_channel_1.default.channel('undici:request:trailers').subscribe((message, name) => {
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
- // diagnosticsChannel.channel('undici:request:error')
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;
@@ -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: {
@@ -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;
@@ -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
- diagnosticsChannel.channel('undici:request:create').subscribe((message, name) => {
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
- diagnosticsChannel.channel('undici:client:connected').subscribe((message, name) => {
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
- debug('[%s] Socket#%d connected', name, socket[symbols.kSocketId]);
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
- diagnosticsChannel.channel('undici:client:sendHeaders').subscribe((message, name) => {
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
- diagnosticsChannel.channel('undici:request:bodySent').subscribe((message, name) => {
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
- diagnosticsChannel.channel('undici:request:headers').subscribe((message, name) => {
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
- diagnosticsChannel.channel('undici:request:trailers').subscribe((message, name) => {
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
- // diagnosticsChannel.channel('undici:request:error')
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
  }
@@ -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);
@@ -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.16.1');
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
- const requestStartTime = performance.now();
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
- debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s', requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout);
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.localAddress;
554
- socketInfo.localPort = socket.localPort;
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
  }