undici 7.0.0-alpha.6 → 7.0.0-alpha.7

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.
@@ -1,5 +1,8 @@
1
1
  'use strict'
2
2
  const EventEmitter = require('node:events')
3
+ const WrapHandler = require('../handler/wrap-handler')
4
+
5
+ const wrapInterceptor = (dispatch) => (opts, handler) => dispatch(opts, WrapHandler.wrap(handler))
3
6
 
4
7
  class Dispatcher extends EventEmitter {
5
8
  dispatch () {
@@ -29,6 +32,7 @@ class Dispatcher extends EventEmitter {
29
32
  }
30
33
 
31
34
  dispatch = interceptor(dispatch)
35
+ dispatch = wrapInterceptor(dispatch)
32
36
 
33
37
  if (dispatch == null || typeof dispatch !== 'function' || dispatch.length !== 2) {
34
38
  throw new TypeError('invalid interceptor')
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const util = require('../core/util')
4
- const DecoratorHandler = require('../handler/decorator-handler')
5
4
  const {
6
5
  parseCacheControlHeader,
7
6
  parseVaryHeader,
@@ -11,9 +10,9 @@ const {
11
10
  function noop () {}
12
11
 
13
12
  /**
14
- * Writes a response to a CacheStore and then passes it on to the next handler
13
+ * @implements {import('../../types/dispatcher.d.ts').default.DispatchHandler}
15
14
  */
16
- class CacheHandler extends DecoratorHandler {
15
+ class CacheHandler {
17
16
  /**
18
17
  * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
19
18
  */
@@ -25,7 +24,7 @@ class CacheHandler extends DecoratorHandler {
25
24
  #store
26
25
 
27
26
  /**
28
- * @type {import('../../types/dispatcher.d.ts').default.DispatchHandlers}
27
+ * @type {import('../../types/dispatcher.d.ts').default.DispatchHandler}
29
28
  */
30
29
  #handler
31
30
 
@@ -35,58 +34,41 @@ class CacheHandler extends DecoratorHandler {
35
34
  #writeStream
36
35
 
37
36
  /**
38
- * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} opts
37
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts
39
38
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
40
- * @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
39
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
41
40
  */
42
41
  constructor (opts, cacheKey, handler) {
43
42
  const { store } = opts
44
43
 
45
- super(handler)
46
-
47
44
  this.#store = store
48
45
  this.#cacheKey = cacheKey
49
46
  this.#handler = handler
50
47
  }
51
48
 
52
- onConnect (abort) {
53
- if (this.#writeStream) {
54
- this.#writeStream.destroy()
55
- this.#writeStream = undefined
56
- }
49
+ onRequestStart (controller, context) {
50
+ this.#writeStream?.destroy()
51
+ this.#writeStream = undefined
52
+ this.#handler.onRequestStart?.(controller, context)
53
+ }
57
54
 
58
- if (typeof this.#handler.onConnect === 'function') {
59
- this.#handler.onConnect(abort)
60
- }
55
+ onRequestUpgrade (controller, statusCode, headers, socket) {
56
+ this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
61
57
  }
62
58
 
63
- /**
64
- * @see {DispatchHandlers.onHeaders}
65
- *
66
- * @param {number} statusCode
67
- * @param {Buffer[]} rawHeaders
68
- * @param {() => void} resume
69
- * @param {string} statusMessage
70
- * @returns {boolean}
71
- */
72
- onHeaders (
59
+ onResponseStart (
60
+ controller,
73
61
  statusCode,
74
- rawHeaders,
75
- resume,
76
- statusMessage
62
+ statusMessage,
63
+ headers
77
64
  ) {
78
- const downstreamOnHeaders = () => {
79
- if (typeof this.#handler.onHeaders === 'function') {
80
- return this.#handler.onHeaders(
81
- statusCode,
82
- rawHeaders,
83
- resume,
84
- statusMessage
85
- )
86
- } else {
87
- return true
88
- }
89
- }
65
+ const downstreamOnHeaders = () =>
66
+ this.#handler.onResponseStart?.(
67
+ controller,
68
+ statusCode,
69
+ statusMessage,
70
+ headers
71
+ )
90
72
 
91
73
  if (
92
74
  !util.safeHTTPMethods.includes(this.#cacheKey.method) &&
@@ -102,21 +84,12 @@ class CacheHandler extends DecoratorHandler {
102
84
  return downstreamOnHeaders()
103
85
  }
104
86
 
105
- const parsedRawHeaders = util.parseRawHeaders(rawHeaders)
106
- const headers = util.parseHeaders(parsedRawHeaders)
107
-
108
87
  const cacheControlHeader = headers['cache-control']
109
- const isCacheFull = typeof this.#store.isFull !== 'undefined'
110
- ? this.#store.isFull
111
- : false
112
-
113
- if (
114
- !cacheControlHeader ||
115
- isCacheFull
116
- ) {
88
+ if (!cacheControlHeader) {
117
89
  // Don't have the cache control header or the cache is full
118
90
  return downstreamOnHeaders()
119
91
  }
92
+
120
93
  const cacheControlDirectives = parseCacheControlHeader(cacheControlHeader)
121
94
  if (!canCacheResponse(statusCode, headers, cacheControlDirectives)) {
122
95
  return downstreamOnHeaders()
@@ -130,11 +103,7 @@ class CacheHandler extends DecoratorHandler {
130
103
  : undefined
131
104
  const deleteAt = determineDeleteAt(now, cacheControlDirectives, staleAt)
132
105
 
133
- const strippedHeaders = stripNecessaryHeaders(
134
- rawHeaders,
135
- parsedRawHeaders,
136
- cacheControlDirectives
137
- )
106
+ const strippedHeaders = stripNecessaryHeaders(headers, cacheControlDirectives)
138
107
 
139
108
  /**
140
109
  * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
@@ -142,7 +111,7 @@ class CacheHandler extends DecoratorHandler {
142
111
  const value = {
143
112
  statusCode,
144
113
  statusMessage,
145
- rawHeaders: strippedHeaders,
114
+ headers: strippedHeaders,
146
115
  vary: varyDirectives,
147
116
  cachedAt: now,
148
117
  staleAt,
@@ -158,7 +127,7 @@ class CacheHandler extends DecoratorHandler {
158
127
  if (this.#writeStream) {
159
128
  const handler = this
160
129
  this.#writeStream
161
- .on('drain', resume)
130
+ .on('drain', () => controller.resume())
162
131
  .on('error', function () {
163
132
  // TODO (fix): Make error somehow observable?
164
133
  })
@@ -168,7 +137,7 @@ class CacheHandler extends DecoratorHandler {
168
137
  }
169
138
 
170
139
  // TODO (fix): Should we resume even if was paused downstream?
171
- resume()
140
+ controller.resume()
172
141
  })
173
142
  }
174
143
  }
@@ -176,55 +145,23 @@ class CacheHandler extends DecoratorHandler {
176
145
  return downstreamOnHeaders()
177
146
  }
178
147
 
179
- /**
180
- * @see {DispatchHandlers.onData}
181
- *
182
- * @param {Buffer} chunk
183
- * @returns {boolean}
184
- */
185
- onData (chunk) {
186
- let paused = false
187
-
188
- if (this.#writeStream) {
189
- paused ||= this.#writeStream.write(chunk) === false
148
+ onResponseData (controller, chunk) {
149
+ if (this.#writeStream?.write(chunk) === false) {
150
+ controller.pause()
190
151
  }
191
152
 
192
- if (typeof this.#handler.onData === 'function') {
193
- paused ||= this.#handler.onData(chunk) === false
194
- }
195
-
196
- return !paused
153
+ this.#handler.onResponseData?.(controller, chunk)
197
154
  }
198
155
 
199
- /**
200
- * @see {DispatchHandlers.onComplete}
201
- *
202
- * @param {string[] | null} rawTrailers
203
- */
204
- onComplete (rawTrailers) {
205
- if (this.#writeStream) {
206
- this.#writeStream.end()
207
- }
208
-
209
- if (typeof this.#handler.onComplete === 'function') {
210
- return this.#handler.onComplete(rawTrailers)
211
- }
156
+ onResponseEnd (controller, trailers) {
157
+ this.#writeStream?.end()
158
+ this.#handler.onResponseEnd?.(controller, trailers)
212
159
  }
213
160
 
214
- /**
215
- * @see {DispatchHandlers.onError}
216
- *
217
- * @param {Error} err
218
- */
219
- onError (err) {
220
- if (this.#writeStream) {
221
- this.#writeStream.destroy(err)
222
- this.#writeStream = undefined
223
- }
224
-
225
- if (typeof this.#handler.onError === 'function') {
226
- this.#handler.onError(err)
227
- }
161
+ onResponseError (controller, err) {
162
+ this.#writeStream?.destroy(err)
163
+ this.#writeStream = undefined
164
+ this.#handler.onResponseError?.(controller, err)
228
165
  }
229
166
  }
230
167
 
@@ -236,10 +173,7 @@ class CacheHandler extends DecoratorHandler {
236
173
  * @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
237
174
  */
238
175
  function canCacheResponse (statusCode, headers, cacheControlDirectives) {
239
- if (
240
- statusCode !== 200 &&
241
- statusCode !== 307
242
- ) {
176
+ if (statusCode !== 200 && statusCode !== 307) {
243
177
  return false
244
178
  }
245
179
 
@@ -309,7 +243,7 @@ function determineStaleAt (now, headers, cacheControlDirectives) {
309
243
  if (headers.expire && typeof headers.expire === 'string') {
310
244
  // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
311
245
  const expiresDate = new Date(headers.expire)
312
- if (expiresDate instanceof Date && !isNaN(expiresDate)) {
246
+ if (expiresDate instanceof Date && Number.isFinite(expiresDate.valueOf())) {
313
247
  return now + (Date.now() - expiresDate.getTime())
314
248
  }
315
249
  }
@@ -332,12 +266,11 @@ function determineDeleteAt (now, cacheControlDirectives, staleAt) {
332
266
 
333
267
  /**
334
268
  * Strips headers required to be removed in cached responses
335
- * @param {Buffer[]} rawHeaders
336
- * @param {string[]} parsedRawHeaders
269
+ * @param {Record<string, string | string[]>} headers
337
270
  * @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
338
- * @returns {Buffer[]}
271
+ * @returns {Record<string, string | string []>}
339
272
  */
340
- function stripNecessaryHeaders (rawHeaders, parsedRawHeaders, cacheControlDirectives) {
273
+ function stripNecessaryHeaders (headers, cacheControlDirectives) {
341
274
  const headersToRemove = ['connection']
342
275
 
343
276
  if (Array.isArray(cacheControlDirectives['no-cache'])) {
@@ -349,51 +282,13 @@ function stripNecessaryHeaders (rawHeaders, parsedRawHeaders, cacheControlDirect
349
282
  }
350
283
 
351
284
  let strippedHeaders
352
-
353
- let offset = 0
354
- for (let i = 0; i < parsedRawHeaders.length; i += 2) {
355
- const headerName = parsedRawHeaders[i]
356
-
285
+ for (const headerName of Object.keys(headers)) {
357
286
  if (headersToRemove.includes(headerName)) {
358
- // We have at least one header we want to remove
359
- if (!strippedHeaders) {
360
- // This is the first header we want to remove, let's create the array
361
- // Since we're stripping headers, this will over allocate. We'll trim
362
- // it later.
363
- strippedHeaders = new Array(parsedRawHeaders.length)
364
-
365
- // Backfill the previous headers into it
366
- for (let j = 0; j < i; j += 2) {
367
- strippedHeaders[j] = parsedRawHeaders[j]
368
- strippedHeaders[j + 1] = parsedRawHeaders[j + 1]
369
- }
370
- }
371
-
372
- // We can't map indices 1:1 from stripped headers to rawHeaders without
373
- // creating holes (if we skip a header, we now have two holes where at
374
- // element should be). So, let's keep an offset to keep strippedHeaders
375
- // flattened. We can also use this at the end for trimming the empty
376
- // elements off of strippedHeaders.
377
- offset += 2
378
-
379
- continue
380
- }
381
-
382
- // We want to keep this header. Let's add it to strippedHeaders if it exists
383
- if (strippedHeaders) {
384
- strippedHeaders[i - offset] = parsedRawHeaders[i]
385
- strippedHeaders[i + 1 - offset] = parsedRawHeaders[i + 1]
287
+ strippedHeaders ??= { ...headers }
288
+ delete headers[headerName]
386
289
  }
387
290
  }
388
-
389
- if (strippedHeaders) {
390
- // Trim off the empty values at the end
391
- strippedHeaders.length -= offset
392
- }
393
-
394
- return strippedHeaders
395
- ? util.encodeRawHeaders(strippedHeaders)
396
- : rawHeaders
291
+ return strippedHeaders ?? headers
397
292
  }
398
293
 
399
294
  module.exports = CacheHandler
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const assert = require('node:assert')
4
- const DecoratorHandler = require('../handler/decorator-handler')
5
4
 
6
5
  /**
7
6
  * This takes care of revalidation requests we send to the origin. If we get
@@ -14,123 +13,86 @@ const DecoratorHandler = require('../handler/decorator-handler')
14
13
  *
15
14
  * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-validation
16
15
  *
17
- * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandlers} DispatchHandlers
18
- * @implements {DispatchHandlers}
16
+ * @implements {import('../../types/dispatcher.d.ts').default.DispatchHandler}
19
17
  */
20
- class CacheRevalidationHandler extends DecoratorHandler {
18
+ class CacheRevalidationHandler {
21
19
  #successful = false
22
20
  /**
23
- * @type {((boolean) => void) | null}
21
+ * @type {((boolean, any) => void) | null}
24
22
  */
25
23
  #callback
26
24
  /**
27
- * @type {(import('../../types/dispatcher.d.ts').default.DispatchHandlers)}
25
+ * @type {(import('../../types/dispatcher.d.ts').default.DispatchHandler)}
28
26
  */
29
27
  #handler
30
28
 
31
- #abort
29
+ #context
32
30
 
33
31
  /**
34
- * @param {(boolean) => void} callback Function to call if the cached value is valid
35
- * @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
32
+ * @param {(boolean, any) => void} callback Function to call if the cached value is valid
33
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchHandler} handler
36
34
  */
37
35
  constructor (callback, handler) {
38
36
  if (typeof callback !== 'function') {
39
37
  throw new TypeError('callback must be a function')
40
38
  }
41
39
 
42
- super(handler)
43
-
44
40
  this.#callback = callback
45
41
  this.#handler = handler
46
42
  }
47
43
 
48
- onConnect (abort) {
44
+ onRequestStart (controller, context) {
49
45
  this.#successful = false
50
- this.#abort = abort
46
+ this.#context = context
51
47
  }
52
48
 
53
- /**
54
- * @see {DispatchHandlers.onHeaders}
55
- *
56
- * @param {number} statusCode
57
- * @param {Buffer[]} rawHeaders
58
- * @param {() => void} resume
59
- * @param {string} statusMessage
60
- * @returns {boolean}
61
- */
62
- onHeaders (
49
+ onRequestUpgrade (controller, statusCode, headers, socket) {
50
+ this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
51
+ }
52
+
53
+ onResponseStart (
54
+ controller,
63
55
  statusCode,
64
- rawHeaders,
65
- resume,
66
- statusMessage
56
+ statusMessage,
57
+ headers
67
58
  ) {
68
59
  assert(this.#callback != null)
69
60
 
70
61
  // https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-a-validation-respo
71
62
  this.#successful = statusCode === 304
72
- this.#callback(this.#successful)
63
+ this.#callback(this.#successful, this.#context)
73
64
  this.#callback = null
74
65
 
75
66
  if (this.#successful) {
76
67
  return true
77
68
  }
78
69
 
79
- if (typeof this.#handler.onConnect === 'function') {
80
- this.#handler.onConnect(this.#abort)
81
- }
82
-
83
- if (typeof this.#handler.onHeaders === 'function') {
84
- return this.#handler.onHeaders(
85
- statusCode,
86
- rawHeaders,
87
- resume,
88
- statusMessage
89
- )
90
- }
91
-
92
- return true
70
+ this.#handler.onRequestStart?.(controller, this.#context)
71
+ this.#handler.onResponseStart?.(
72
+ controller,
73
+ statusCode,
74
+ statusMessage,
75
+ headers
76
+ )
93
77
  }
94
78
 
95
- /**
96
- * @see {DispatchHandlers.onData}
97
- *
98
- * @param {Buffer} chunk
99
- * @returns {boolean}
100
- */
101
- onData (chunk) {
79
+ onResponseData (controller, chunk) {
102
80
  if (this.#successful) {
103
- return true
104
- }
105
-
106
- if (typeof this.#handler.onData === 'function') {
107
- return this.#handler.onData(chunk)
81
+ return
108
82
  }
109
83
 
110
- return true
84
+ return this.#handler.onResponseData(controller, chunk)
111
85
  }
112
86
 
113
- /**
114
- * @see {DispatchHandlers.onComplete}
115
- *
116
- * @param {string[] | null} rawTrailers
117
- */
118
- onComplete (rawTrailers) {
87
+ onResponseEnd (controller, trailers) {
119
88
  if (this.#successful) {
120
89
  return
121
90
  }
122
91
 
123
- if (typeof this.#handler.onComplete === 'function') {
124
- this.#handler.onComplete(rawTrailers)
125
- }
92
+ this.#handler.onResponseEnd?.(controller, trailers)
126
93
  }
127
94
 
128
- /**
129
- * @see {DispatchHandlers.onError}
130
- *
131
- * @param {Error} err
132
- */
133
- onError (err) {
95
+ onResponseError (controller, err) {
134
96
  if (this.#successful) {
135
97
  return
136
98
  }
@@ -140,8 +102,8 @@ class CacheRevalidationHandler extends DecoratorHandler {
140
102
  this.#callback = null
141
103
  }
142
104
 
143
- if (typeof this.#handler.onError === 'function') {
144
- this.#handler.onError(err)
105
+ if (typeof this.#handler.onResponseError === 'function') {
106
+ this.#handler.onResponseError(controller, err)
145
107
  } else {
146
108
  throw err
147
109
  }
@@ -10,6 +10,8 @@ const redirectableStatusCodes = [300, 301, 302, 303, 307, 308]
10
10
 
11
11
  const kBody = Symbol('body')
12
12
 
13
+ const noop = () => {}
14
+
13
15
  class BodyAsyncIterable {
14
16
  constructor (body) {
15
17
  this[kBody] = body
@@ -38,8 +40,6 @@ class RedirectHandler {
38
40
  throw new InvalidArgumentError('maxRedirections must be a positive number')
39
41
  }
40
42
 
41
- util.assertRequestHandler(handler, opts.method, opts.upgrade)
42
-
43
43
  this.dispatch = dispatch
44
44
  this.location = null
45
45
  this.abort = null
@@ -47,7 +47,6 @@ class RedirectHandler {
47
47
  this.maxRedirections = maxRedirections
48
48
  this.handler = handler
49
49
  this.history = []
50
- this.redirectionLimitReached = false
51
50
 
52
51
  if (util.isStream(this.opts.body)) {
53
52
  // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
@@ -97,27 +96,42 @@ class RedirectHandler {
97
96
  this.handler.onError(error)
98
97
  }
99
98
 
100
- onHeaders (statusCode, headers, resume, statusText) {
101
- this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body)
102
- ? null
103
- : parseLocation(statusCode, headers)
104
-
99
+ onHeaders (statusCode, rawHeaders, resume, statusText) {
105
100
  if (this.opts.throwOnMaxRedirect && this.history.length >= this.maxRedirections) {
106
- if (this.request) {
107
- this.request.abort(new Error('max redirects'))
101
+ throw new Error('max redirects')
102
+ }
103
+
104
+ // https://tools.ietf.org/html/rfc7231#section-6.4.2
105
+ // https://fetch.spec.whatwg.org/#http-redirect-fetch
106
+ // In case of HTTP 301 or 302 with POST, change the method to GET
107
+ if ((statusCode === 301 || statusCode === 302) && this.opts.method === 'POST') {
108
+ this.opts.method = 'GET'
109
+ if (util.isStream(this.opts.body)) {
110
+ util.destroy(this.opts.body.on('error', noop))
108
111
  }
112
+ this.opts.body = null
113
+ }
109
114
 
110
- this.redirectionLimitReached = true
111
- this.abort(new Error('max redirects'))
112
- return
115
+ // https://tools.ietf.org/html/rfc7231#section-6.4.4
116
+ // In case of HTTP 303, always replace method to be either HEAD or GET
117
+ if (statusCode === 303 && this.opts.method !== 'HEAD') {
118
+ this.opts.method = 'GET'
119
+ if (util.isStream(this.opts.body)) {
120
+ util.destroy(this.opts.body.on('error', noop))
121
+ }
122
+ this.opts.body = null
113
123
  }
114
124
 
125
+ this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body)
126
+ ? null
127
+ : parseLocation(statusCode, rawHeaders)
128
+
115
129
  if (this.opts.origin) {
116
130
  this.history.push(new URL(this.opts.path, this.opts.origin))
117
131
  }
118
132
 
119
133
  if (!this.location) {
120
- return this.handler.onHeaders(statusCode, headers, resume, statusText)
134
+ return this.handler.onHeaders(statusCode, rawHeaders, resume, statusText)
121
135
  }
122
136
 
123
137
  const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
@@ -131,13 +145,6 @@ class RedirectHandler {
131
145
  this.opts.origin = origin
132
146
  this.opts.maxRedirections = 0
133
147
  this.opts.query = null
134
-
135
- // https://tools.ietf.org/html/rfc7231#section-6.4.4
136
- // In case of HTTP 303, always replace method to be either HEAD or GET
137
- if (statusCode === 303 && this.opts.method !== 'HEAD') {
138
- this.opts.method = 'GET'
139
- this.opts.body = null
140
- }
141
148
  }
142
149
 
143
150
  onData (chunk) {
@@ -191,14 +198,14 @@ class RedirectHandler {
191
198
  }
192
199
  }
193
200
 
194
- function parseLocation (statusCode, headers) {
201
+ function parseLocation (statusCode, rawHeaders) {
195
202
  if (redirectableStatusCodes.indexOf(statusCode) === -1) {
196
203
  return null
197
204
  }
198
205
 
199
- for (let i = 0; i < headers.length; i += 2) {
200
- if (headers[i].length === 8 && util.headerNameToString(headers[i]) === 'location') {
201
- return headers[i + 1]
206
+ for (let i = 0; i < rawHeaders.length; i += 2) {
207
+ if (rawHeaders[i].length === 8 && util.headerNameToString(rawHeaders[i]) === 'location') {
208
+ return rawHeaders[i + 1]
202
209
  }
203
210
  }
204
211
  }
@@ -37,6 +37,7 @@ class RetryHandler {
37
37
  this.opts = { ...dispatchOpts, body: wrapRequestBody(opts.body) }
38
38
  this.abort = null
39
39
  this.aborted = false
40
+ this.connectCalled = false
40
41
  this.retryOpts = {
41
42
  retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry],
42
43
  retryAfter: retryAfter ?? true,
@@ -68,16 +69,6 @@ class RetryHandler {
68
69
  this.end = null
69
70
  this.etag = null
70
71
  this.resume = null
71
-
72
- // Handle possible onConnect duplication
73
- this.handler.onConnect(reason => {
74
- this.aborted = true
75
- if (this.abort) {
76
- this.abort(reason)
77
- } else {
78
- this.reason = reason
79
- }
80
- })
81
72
  }
82
73
 
83
74
  onRequestSent () {
@@ -92,11 +83,14 @@ class RetryHandler {
92
83
  }
93
84
  }
94
85
 
95
- onConnect (abort) {
96
- if (this.aborted) {
97
- abort(this.reason)
98
- } else {
99
- this.abort = abort
86
+ onConnect (abort, context) {
87
+ this.abort = abort
88
+ if (!this.connectCalled) {
89
+ this.connectCalled = true
90
+ this.handler.onConnect(reason => {
91
+ this.aborted = true
92
+ this.abort(reason)
93
+ }, context)
100
94
  }
101
95
  }
102
96