undici 6.19.8 → 6.20.0

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.
@@ -984,6 +984,203 @@ client.dispatch(
984
984
  );
985
985
  ```
986
986
 
987
+ ##### `Response Error Interceptor`
988
+
989
+ **Introduction**
990
+
991
+ The Response Error Interceptor is designed to handle HTTP response errors efficiently. It intercepts responses and throws detailed errors for responses with status codes indicating failure (4xx, 5xx). This interceptor enhances error handling by providing structured error information, including response headers, data, and status codes.
992
+
993
+ **ResponseError Class**
994
+
995
+ The `ResponseError` class extends the `UndiciError` class and encapsulates detailed error information. It captures the response status code, headers, and data, providing a structured way to handle errors.
996
+
997
+ **Definition**
998
+
999
+ ```js
1000
+ class ResponseError extends UndiciError {
1001
+ constructor (message, code, { headers, data }) {
1002
+ super(message);
1003
+ this.name = 'ResponseError';
1004
+ this.message = message || 'Response error';
1005
+ this.code = 'UND_ERR_RESPONSE';
1006
+ this.statusCode = code;
1007
+ this.data = data;
1008
+ this.headers = headers;
1009
+ }
1010
+ }
1011
+ ```
1012
+
1013
+ **Interceptor Handler**
1014
+
1015
+ The interceptor's handler class extends `DecoratorHandler` and overrides methods to capture response details and handle errors based on the response status code.
1016
+
1017
+ **Methods**
1018
+
1019
+ - **onConnect**: Initializes response properties.
1020
+ - **onHeaders**: Captures headers and status code. Decodes body if content type is `application/json` or `text/plain`.
1021
+ - **onData**: Appends chunks to the body if status code indicates an error.
1022
+ - **onComplete**: Finalizes error handling, constructs a `ResponseError`, and invokes the `onError` method.
1023
+ - **onError**: Propagates errors to the handler.
1024
+
1025
+ **Definition**
1026
+
1027
+ ```js
1028
+ class Handler extends DecoratorHandler {
1029
+ // Private properties
1030
+ #handler;
1031
+ #statusCode;
1032
+ #contentType;
1033
+ #decoder;
1034
+ #headers;
1035
+ #body;
1036
+
1037
+ constructor (opts, { handler }) {
1038
+ super(handler);
1039
+ this.#handler = handler;
1040
+ }
1041
+
1042
+ onConnect (abort) {
1043
+ this.#statusCode = 0;
1044
+ this.#contentType = null;
1045
+ this.#decoder = null;
1046
+ this.#headers = null;
1047
+ this.#body = '';
1048
+ return this.#handler.onConnect(abort);
1049
+ }
1050
+
1051
+ onHeaders (statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
1052
+ this.#statusCode = statusCode;
1053
+ this.#headers = headers;
1054
+ this.#contentType = headers['content-type'];
1055
+
1056
+ if (this.#statusCode < 400) {
1057
+ return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers);
1058
+ }
1059
+
1060
+ if (this.#contentType === 'application/json' || this.#contentType === 'text/plain') {
1061
+ this.#decoder = new TextDecoder('utf-8');
1062
+ }
1063
+ }
1064
+
1065
+ onData (chunk) {
1066
+ if (this.#statusCode < 400) {
1067
+ return this.#handler.onData(chunk);
1068
+ }
1069
+ this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? '';
1070
+ }
1071
+
1072
+ onComplete (rawTrailers) {
1073
+ if (this.#statusCode >= 400) {
1074
+ this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? '';
1075
+ if (this.#contentType === 'application/json') {
1076
+ try {
1077
+ this.#body = JSON.parse(this.#body);
1078
+ } catch {
1079
+ // Do nothing...
1080
+ }
1081
+ }
1082
+
1083
+ let err;
1084
+ const stackTraceLimit = Error.stackTraceLimit;
1085
+ Error.stackTraceLimit = 0;
1086
+ try {
1087
+ err = new ResponseError('Response Error', this.#statusCode, this.#headers, this.#body);
1088
+ } finally {
1089
+ Error.stackTraceLimit = stackTraceLimit;
1090
+ }
1091
+
1092
+ this.#handler.onError(err);
1093
+ } else {
1094
+ this.#handler.onComplete(rawTrailers);
1095
+ }
1096
+ }
1097
+
1098
+ onError (err) {
1099
+ this.#handler.onError(err);
1100
+ }
1101
+ }
1102
+
1103
+ module.exports = (dispatch) => (opts, handler) => opts.throwOnError
1104
+ ? dispatch(opts, new Handler(opts, { handler }))
1105
+ : dispatch(opts, handler);
1106
+ ```
1107
+
1108
+ **Tests**
1109
+
1110
+ Unit tests ensure the interceptor functions correctly, handling both error and non-error responses appropriately.
1111
+
1112
+ **Example Tests**
1113
+
1114
+ - **No Error if `throwOnError` is False**:
1115
+
1116
+ ```js
1117
+ test('should not error if request is not meant to throw error', async (t) => {
1118
+ const opts = { throwOnError: false };
1119
+ const handler = { onError: () => {}, onData: () => {}, onComplete: () => {} };
1120
+ const interceptor = createResponseErrorInterceptor((opts, handler) => handler.onComplete());
1121
+ assert.doesNotThrow(() => interceptor(opts, handler));
1122
+ });
1123
+ ```
1124
+
1125
+ - **Error if Status Code is in Specified Error Codes**:
1126
+
1127
+ ```js
1128
+ test('should error if request status code is in the specified error codes', async (t) => {
1129
+ const opts = { throwOnError: true, statusCodes: [500] };
1130
+ const response = { statusCode: 500 };
1131
+ let capturedError;
1132
+ const handler = {
1133
+ onError: (err) => { capturedError = err; },
1134
+ onData: () => {},
1135
+ onComplete: () => {}
1136
+ };
1137
+
1138
+ const interceptor = createResponseErrorInterceptor((opts, handler) => {
1139
+ if (opts.throwOnError && opts.statusCodes.includes(response.statusCode)) {
1140
+ handler.onError(new Error('Response Error'));
1141
+ } else {
1142
+ handler.onComplete();
1143
+ }
1144
+ });
1145
+
1146
+ interceptor({ ...opts, response }, handler);
1147
+
1148
+ await new Promise(resolve => setImmediate(resolve));
1149
+
1150
+ assert(capturedError, 'Expected error to be captured but it was not.');
1151
+ assert.strictEqual(capturedError.message, 'Response Error');
1152
+ assert.strictEqual(response.statusCode, 500);
1153
+ });
1154
+ ```
1155
+
1156
+ - **No Error if Status Code is Not in Specified Error Codes**:
1157
+
1158
+ ```js
1159
+ test('should not error if request status code is not in the specified error codes', async (t) => {
1160
+ const opts = { throwOnError: true, statusCodes: [500] };
1161
+ const response = { statusCode: 404 };
1162
+ const handler = {
1163
+ onError: () => {},
1164
+ onData: () => {},
1165
+ onComplete: () => {}
1166
+ };
1167
+
1168
+ const interceptor = createResponseErrorInterceptor((opts, handler) => {
1169
+ if (opts.throwOnError && opts.statusCodes.includes(response.statusCode)) {
1170
+ handler.onError(new Error('Response Error'));
1171
+ } else {
1172
+ handler.onComplete();
1173
+ }
1174
+ });
1175
+
1176
+ assert.doesNotThrow(() => interceptor({ ...opts, response }, handler));
1177
+ });
1178
+ ```
1179
+
1180
+ **Conclusion**
1181
+
1182
+ The Response Error Interceptor provides a robust mechanism for handling HTTP response errors by capturing detailed error information and propagating it through a structured `ResponseError` class. This enhancement improves error handling and debugging capabilities in applications using the interceptor.
1183
+
987
1184
  ## Instance Events
988
1185
 
989
1186
  ### Event: `'connect'`
@@ -19,7 +19,7 @@ Extends: [`Dispatch.DispatchOptions`](Dispatcher.md#parameter-dispatchoptions).
19
19
 
20
20
  #### `RetryOptions`
21
21
 
22
- - **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => void` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
22
+ - **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => number | null` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
23
23
  - **maxRetries** `number` (optional) - Maximum number of retries. Default: `5`
24
24
  - **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds)
25
25
  - **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second)
@@ -50,9 +50,9 @@ class UpgradeHandler extends AsyncResource {
50
50
  }
51
51
 
52
52
  onUpgrade (statusCode, rawHeaders, socket) {
53
- const { callback, opaque, context } = this
53
+ assert(statusCode === 101)
54
54
 
55
- assert.strictEqual(statusCode, 101)
55
+ const { callback, opaque, context } = this
56
56
 
57
57
  removeSignal(this)
58
58
 
@@ -4,6 +4,9 @@ const net = require('node:net')
4
4
  const assert = require('node:assert')
5
5
  const util = require('./util')
6
6
  const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
7
+ const timers = require('../util/timers')
8
+
9
+ function noop () {}
7
10
 
8
11
  let tls // include tls conditionally since it is not always available
9
12
 
@@ -91,9 +94,11 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
91
94
  servername = servername || options.servername || util.getServerName(host) || null
92
95
 
93
96
  const sessionKey = servername || hostname
97
+ assert(sessionKey)
98
+
94
99
  const session = customSession || sessionCache.get(sessionKey) || null
95
100
 
96
- assert(sessionKey)
101
+ port = port || 443
97
102
 
98
103
  socket = tls.connect({
99
104
  highWaterMark: 16384, // TLS in node can't have bigger HWM anyway...
@@ -104,7 +109,7 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
104
109
  // TODO(HTTP/2): Add support for h2c
105
110
  ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'],
106
111
  socket: httpSocket, // upgrade socket connection
107
- port: port || 443,
112
+ port,
108
113
  host: hostname
109
114
  })
110
115
 
@@ -115,11 +120,14 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
115
120
  })
116
121
  } else {
117
122
  assert(!httpSocket, 'httpSocket can only be sent on TLS update')
123
+
124
+ port = port || 80
125
+
118
126
  socket = net.connect({
119
127
  highWaterMark: 64 * 1024, // Same as nodejs fs streams.
120
128
  ...options,
121
129
  localAddress,
122
- port: port || 80,
130
+ port,
123
131
  host: hostname
124
132
  })
125
133
  }
@@ -130,12 +138,12 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
130
138
  socket.setKeepAlive(true, keepAliveInitialDelay)
131
139
  }
132
140
 
133
- const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout)
141
+ const clearConnectTimeout = setupConnectTimeout(new WeakRef(socket), { timeout, hostname, port })
134
142
 
135
143
  socket
136
144
  .setNoDelay(true)
137
145
  .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
138
- cancelTimeout()
146
+ queueMicrotask(clearConnectTimeout)
139
147
 
140
148
  if (callback) {
141
149
  const cb = callback
@@ -144,7 +152,7 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
144
152
  }
145
153
  })
146
154
  .on('error', function (err) {
147
- cancelTimeout()
155
+ queueMicrotask(clearConnectTimeout)
148
156
 
149
157
  if (callback) {
150
158
  const cb = callback
@@ -157,36 +165,70 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
157
165
  }
158
166
  }
159
167
 
160
- function setupTimeout (onConnectTimeout, timeout) {
161
- if (!timeout) {
162
- return () => {}
163
- }
168
+ /**
169
+ * @param {WeakRef<net.Socket>} socketWeakRef
170
+ * @param {object} opts
171
+ * @param {number} opts.timeout
172
+ * @param {string} opts.hostname
173
+ * @param {number} opts.port
174
+ * @returns {() => void}
175
+ */
176
+ const setupConnectTimeout = process.platform === 'win32'
177
+ ? (socketWeakRef, opts) => {
178
+ if (!opts.timeout) {
179
+ return noop
180
+ }
164
181
 
165
- let s1 = null
166
- let s2 = null
167
- const timeoutId = setTimeout(() => {
168
- // setImmediate is added to make sure that we prioritize socket error events over timeouts
169
- s1 = setImmediate(() => {
170
- if (process.platform === 'win32') {
182
+ let s1 = null
183
+ let s2 = null
184
+ const fastTimer = timers.setFastTimeout(() => {
185
+ // setImmediate is added to make sure that we prioritize socket error events over timeouts
186
+ s1 = setImmediate(() => {
171
187
  // Windows needs an extra setImmediate probably due to implementation differences in the socket logic
172
- s2 = setImmediate(() => onConnectTimeout())
173
- } else {
174
- onConnectTimeout()
188
+ s2 = setImmediate(() => onConnectTimeout(socketWeakRef.deref(), opts))
189
+ })
190
+ }, opts.timeout)
191
+ return () => {
192
+ timers.clearFastTimeout(fastTimer)
193
+ clearImmediate(s1)
194
+ clearImmediate(s2)
195
+ }
196
+ }
197
+ : (socketWeakRef, opts) => {
198
+ if (!opts.timeout) {
199
+ return noop
175
200
  }
176
- })
177
- }, timeout)
178
- return () => {
179
- clearTimeout(timeoutId)
180
- clearImmediate(s1)
181
- clearImmediate(s2)
182
- }
183
- }
184
201
 
185
- function onConnectTimeout (socket) {
202
+ let s1 = null
203
+ const fastTimer = timers.setFastTimeout(() => {
204
+ // setImmediate is added to make sure that we prioritize socket error events over timeouts
205
+ s1 = setImmediate(() => {
206
+ onConnectTimeout(socketWeakRef.deref(), opts)
207
+ })
208
+ }, opts.timeout)
209
+ return () => {
210
+ timers.clearFastTimeout(fastTimer)
211
+ clearImmediate(s1)
212
+ }
213
+ }
214
+
215
+ /**
216
+ * @param {net.Socket} socket
217
+ * @param {object} opts
218
+ * @param {number} opts.timeout
219
+ * @param {string} opts.hostname
220
+ * @param {number} opts.port
221
+ */
222
+ function onConnectTimeout (socket, opts) {
186
223
  let message = 'Connect Timeout Error'
187
224
  if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
188
- message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')})`
225
+ message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},`
226
+ } else {
227
+ message += ` (attempted address: ${opts.hostname}:${opts.port},`
189
228
  }
229
+
230
+ message += ` timeout: ${opts.timeout}ms)`
231
+
190
232
  util.destroy(socket, new ConnectTimeoutError(message))
191
233
  }
192
234
 
@@ -195,6 +195,18 @@ class RequestRetryError extends UndiciError {
195
195
  }
196
196
  }
197
197
 
198
+ class ResponseError extends UndiciError {
199
+ constructor (message, code, { headers, data }) {
200
+ super(message)
201
+ this.name = 'ResponseError'
202
+ this.message = message || 'Response error'
203
+ this.code = 'UND_ERR_RESPONSE'
204
+ this.statusCode = code
205
+ this.data = data
206
+ this.headers = headers
207
+ }
208
+ }
209
+
198
210
  class SecureProxyConnectionError extends UndiciError {
199
211
  constructor (cause, message, options) {
200
212
  super(message, { cause, ...(options ?? {}) })
@@ -227,5 +239,6 @@ module.exports = {
227
239
  BalancedPoolMissingUpstreamError,
228
240
  ResponseExceededMaxSizeError,
229
241
  RequestRetryError,
242
+ ResponseError,
230
243
  SecureProxyConnectionError
231
244
  }
package/lib/core/util.js CHANGED
@@ -233,7 +233,7 @@ function getServerName (host) {
233
233
  return null
234
234
  }
235
235
 
236
- assert.strictEqual(typeof host, 'string')
236
+ assert(typeof host === 'string')
237
237
 
238
238
  const servername = getHostname(host)
239
239
  if (net.isIP(servername)) {
@@ -85,35 +85,35 @@ async function lazyllhttp () {
85
85
  return 0
86
86
  },
87
87
  wasm_on_status: (p, at, len) => {
88
- assert.strictEqual(currentParser.ptr, p)
88
+ assert(currentParser.ptr === p)
89
89
  const start = at - currentBufferPtr + currentBufferRef.byteOffset
90
90
  return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
91
91
  },
92
92
  wasm_on_message_begin: (p) => {
93
- assert.strictEqual(currentParser.ptr, p)
93
+ assert(currentParser.ptr === p)
94
94
  return currentParser.onMessageBegin() || 0
95
95
  },
96
96
  wasm_on_header_field: (p, at, len) => {
97
- assert.strictEqual(currentParser.ptr, p)
97
+ assert(currentParser.ptr === p)
98
98
  const start = at - currentBufferPtr + currentBufferRef.byteOffset
99
99
  return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
100
100
  },
101
101
  wasm_on_header_value: (p, at, len) => {
102
- assert.strictEqual(currentParser.ptr, p)
102
+ assert(currentParser.ptr === p)
103
103
  const start = at - currentBufferPtr + currentBufferRef.byteOffset
104
104
  return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
105
105
  },
106
106
  wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => {
107
- assert.strictEqual(currentParser.ptr, p)
107
+ assert(currentParser.ptr === p)
108
108
  return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0
109
109
  },
110
110
  wasm_on_body: (p, at, len) => {
111
- assert.strictEqual(currentParser.ptr, p)
111
+ assert(currentParser.ptr === p)
112
112
  const start = at - currentBufferPtr + currentBufferRef.byteOffset
113
113
  return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
114
114
  },
115
115
  wasm_on_message_complete: (p) => {
116
- assert.strictEqual(currentParser.ptr, p)
116
+ assert(currentParser.ptr === p)
117
117
  return currentParser.onMessageComplete() || 0
118
118
  }
119
119
 
@@ -131,9 +131,17 @@ let currentBufferRef = null
131
131
  let currentBufferSize = 0
132
132
  let currentBufferPtr = null
133
133
 
134
- const TIMEOUT_HEADERS = 1
135
- const TIMEOUT_BODY = 2
136
- const TIMEOUT_IDLE = 3
134
+ const USE_NATIVE_TIMER = 0
135
+ const USE_FAST_TIMER = 1
136
+
137
+ // Use fast timers for headers and body to take eventual event loop
138
+ // latency into account.
139
+ const TIMEOUT_HEADERS = 2 | USE_FAST_TIMER
140
+ const TIMEOUT_BODY = 4 | USE_FAST_TIMER
141
+
142
+ // Use native timers to ignore event loop latency for keep-alive
143
+ // handling.
144
+ const TIMEOUT_KEEP_ALIVE = 8 | USE_NATIVE_TIMER
137
145
 
138
146
  class Parser {
139
147
  constructor (client, socket, { exports }) {
@@ -164,26 +172,39 @@ class Parser {
164
172
  this.maxResponseSize = client[kMaxResponseSize]
165
173
  }
166
174
 
167
- setTimeout (value, type) {
168
- this.timeoutType = type
169
- if (value !== this.timeoutValue) {
170
- timers.clearTimeout(this.timeout)
171
- if (value) {
172
- this.timeout = timers.setTimeout(onParserTimeout, value, this)
173
- // istanbul ignore else: only for jest
174
- if (this.timeout.unref) {
175
+ setTimeout (delay, type) {
176
+ // If the existing timer and the new timer are of different timer type
177
+ // (fast or native) or have different delay, we need to clear the existing
178
+ // timer and set a new one.
179
+ if (
180
+ delay !== this.timeoutValue ||
181
+ (type & USE_FAST_TIMER) ^ (this.timeoutType & USE_FAST_TIMER)
182
+ ) {
183
+ // If a timeout is already set, clear it with clearTimeout of the fast
184
+ // timer implementation, as it can clear fast and native timers.
185
+ if (this.timeout) {
186
+ timers.clearTimeout(this.timeout)
187
+ this.timeout = null
188
+ }
189
+
190
+ if (delay) {
191
+ if (type & USE_FAST_TIMER) {
192
+ this.timeout = timers.setFastTimeout(onParserTimeout, delay, new WeakRef(this))
193
+ } else {
194
+ this.timeout = setTimeout(onParserTimeout, delay, new WeakRef(this))
175
195
  this.timeout.unref()
176
196
  }
177
- } else {
178
- this.timeout = null
179
197
  }
180
- this.timeoutValue = value
198
+
199
+ this.timeoutValue = delay
181
200
  } else if (this.timeout) {
182
201
  // istanbul ignore else: only for jest
183
202
  if (this.timeout.refresh) {
184
203
  this.timeout.refresh()
185
204
  }
186
205
  }
206
+
207
+ this.timeoutType = type
187
208
  }
188
209
 
189
210
  resume () {
@@ -288,7 +309,7 @@ class Parser {
288
309
  this.llhttp.llhttp_free(this.ptr)
289
310
  this.ptr = null
290
311
 
291
- timers.clearTimeout(this.timeout)
312
+ this.timeout && timers.clearTimeout(this.timeout)
292
313
  this.timeout = null
293
314
  this.timeoutValue = null
294
315
  this.timeoutType = null
@@ -363,20 +384,19 @@ class Parser {
363
384
  const { upgrade, client, socket, headers, statusCode } = this
364
385
 
365
386
  assert(upgrade)
387
+ assert(client[kSocket] === socket)
388
+ assert(!socket.destroyed)
389
+ assert(!this.paused)
390
+ assert((headers.length & 1) === 0)
366
391
 
367
392
  const request = client[kQueue][client[kRunningIdx]]
368
393
  assert(request)
369
-
370
- assert(!socket.destroyed)
371
- assert(socket === client[kSocket])
372
- assert(!this.paused)
373
394
  assert(request.upgrade || request.method === 'CONNECT')
374
395
 
375
396
  this.statusCode = null
376
397
  this.statusText = ''
377
398
  this.shouldKeepAlive = null
378
399
 
379
- assert(this.headers.length % 2 === 0)
380
400
  this.headers = []
381
401
  this.headersSize = 0
382
402
 
@@ -433,7 +453,7 @@ class Parser {
433
453
  return -1
434
454
  }
435
455
 
436
- assert.strictEqual(this.timeoutType, TIMEOUT_HEADERS)
456
+ assert(this.timeoutType === TIMEOUT_HEADERS)
437
457
 
438
458
  this.statusCode = statusCode
439
459
  this.shouldKeepAlive = (
@@ -466,7 +486,7 @@ class Parser {
466
486
  return 2
467
487
  }
468
488
 
469
- assert(this.headers.length % 2 === 0)
489
+ assert((this.headers.length & 1) === 0)
470
490
  this.headers = []
471
491
  this.headersSize = 0
472
492
 
@@ -523,7 +543,7 @@ class Parser {
523
543
  const request = client[kQueue][client[kRunningIdx]]
524
544
  assert(request)
525
545
 
526
- assert.strictEqual(this.timeoutType, TIMEOUT_BODY)
546
+ assert(this.timeoutType === TIMEOUT_BODY)
527
547
  if (this.timeout) {
528
548
  // istanbul ignore else: only for jest
529
549
  if (this.timeout.refresh) {
@@ -556,11 +576,12 @@ class Parser {
556
576
  return
557
577
  }
558
578
 
579
+ assert(statusCode >= 100)
580
+ assert((this.headers.length & 1) === 0)
581
+
559
582
  const request = client[kQueue][client[kRunningIdx]]
560
583
  assert(request)
561
584
 
562
- assert(statusCode >= 100)
563
-
564
585
  this.statusCode = null
565
586
  this.statusText = ''
566
587
  this.bytesRead = 0
@@ -568,7 +589,6 @@ class Parser {
568
589
  this.keepAlive = ''
569
590
  this.connection = ''
570
591
 
571
- assert(this.headers.length % 2 === 0)
572
592
  this.headers = []
573
593
  this.headersSize = 0
574
594
 
@@ -587,7 +607,7 @@ class Parser {
587
607
  client[kQueue][client[kRunningIdx]++] = null
588
608
 
589
609
  if (socket[kWriting]) {
590
- assert.strictEqual(client[kRunning], 0)
610
+ assert(client[kRunning] === 0)
591
611
  // Response completed before request.
592
612
  util.destroy(socket, new InformationalError('reset'))
593
613
  return constants.ERROR.PAUSED
@@ -613,19 +633,19 @@ class Parser {
613
633
  }
614
634
 
615
635
  function onParserTimeout (parser) {
616
- const { socket, timeoutType, client } = parser
636
+ const { socket, timeoutType, client, paused } = parser.deref()
617
637
 
618
638
  /* istanbul ignore else */
619
639
  if (timeoutType === TIMEOUT_HEADERS) {
620
640
  if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
621
- assert(!parser.paused, 'cannot be paused while waiting for headers')
641
+ assert(!paused, 'cannot be paused while waiting for headers')
622
642
  util.destroy(socket, new HeadersTimeoutError())
623
643
  }
624
644
  } else if (timeoutType === TIMEOUT_BODY) {
625
- if (!parser.paused) {
645
+ if (!paused) {
626
646
  util.destroy(socket, new BodyTimeoutError())
627
647
  }
628
- } else if (timeoutType === TIMEOUT_IDLE) {
648
+ } else if (timeoutType === TIMEOUT_KEEP_ALIVE) {
629
649
  assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue])
630
650
  util.destroy(socket, new InformationalError('socket idle timeout'))
631
651
  }
@@ -646,10 +666,10 @@ async function connectH1 (client, socket) {
646
666
  socket[kParser] = new Parser(client, socket, llhttpInstance)
647
667
 
648
668
  addListener(socket, 'error', function (err) {
649
- const parser = this[kParser]
650
-
651
669
  assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
652
670
 
671
+ const parser = this[kParser]
672
+
653
673
  // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
654
674
  // to the user.
655
675
  if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
@@ -803,8 +823,8 @@ function resumeH1 (client) {
803
823
  }
804
824
 
805
825
  if (client[kSize] === 0) {
806
- if (socket[kParser].timeoutType !== TIMEOUT_IDLE) {
807
- socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_IDLE)
826
+ if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
827
+ socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE)
808
828
  }
809
829
  } else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) {
810
830
  if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) {
@@ -385,6 +385,10 @@ function onError (client, err) {
385
385
  }
386
386
  }
387
387
 
388
+ /**
389
+ * @param {Client} client
390
+ * @returns
391
+ */
388
392
  async function connect (client) {
389
393
  assert(!client[kConnecting])
390
394
  assert(!client[kHTTPContext])