undici 7.0.0-alpha.1 → 7.0.0-alpha.10

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.
Files changed (113) hide show
  1. package/README.md +24 -38
  2. package/docs/docs/api/Agent.md +14 -14
  3. package/docs/docs/api/BalancedPool.md +16 -16
  4. package/docs/docs/api/CacheStore.md +131 -0
  5. package/docs/docs/api/Client.md +12 -12
  6. package/docs/docs/api/Debug.md +1 -1
  7. package/docs/docs/api/Dispatcher.md +98 -193
  8. package/docs/docs/api/EnvHttpProxyAgent.md +12 -12
  9. package/docs/docs/api/MockAgent.md +5 -3
  10. package/docs/docs/api/MockClient.md +5 -5
  11. package/docs/docs/api/MockPool.md +4 -3
  12. package/docs/docs/api/Pool.md +15 -15
  13. package/docs/docs/api/PoolStats.md +1 -1
  14. package/docs/docs/api/ProxyAgent.md +3 -3
  15. package/docs/docs/api/RedirectHandler.md +1 -1
  16. package/docs/docs/api/RetryAgent.md +1 -1
  17. package/docs/docs/api/RetryHandler.md +4 -4
  18. package/docs/docs/api/WebSocket.md +46 -4
  19. package/docs/docs/api/api-lifecycle.md +11 -11
  20. package/docs/docs/best-practices/mocking-request.md +2 -2
  21. package/docs/docs/best-practices/proxy.md +1 -1
  22. package/index.d.ts +1 -1
  23. package/index.js +23 -3
  24. package/lib/api/abort-signal.js +2 -0
  25. package/lib/api/api-pipeline.js +4 -2
  26. package/lib/api/api-request.js +6 -4
  27. package/lib/api/api-stream.js +3 -1
  28. package/lib/api/api-upgrade.js +2 -2
  29. package/lib/api/readable.js +200 -47
  30. package/lib/api/util.js +2 -0
  31. package/lib/cache/memory-cache-store.js +177 -0
  32. package/lib/cache/sqlite-cache-store.js +446 -0
  33. package/lib/core/connect.js +54 -22
  34. package/lib/core/constants.js +35 -10
  35. package/lib/core/diagnostics.js +122 -128
  36. package/lib/core/errors.js +2 -2
  37. package/lib/core/request.js +6 -6
  38. package/lib/core/symbols.js +2 -0
  39. package/lib/core/tree.js +4 -2
  40. package/lib/core/util.js +238 -40
  41. package/lib/dispatcher/client-h1.js +405 -142
  42. package/lib/dispatcher/client-h2.js +212 -109
  43. package/lib/dispatcher/client.js +24 -7
  44. package/lib/dispatcher/dispatcher-base.js +4 -1
  45. package/lib/dispatcher/dispatcher.js +4 -0
  46. package/lib/dispatcher/fixed-queue.js +91 -49
  47. package/lib/dispatcher/pool-base.js +3 -3
  48. package/lib/dispatcher/pool-stats.js +2 -0
  49. package/lib/dispatcher/proxy-agent.js +3 -1
  50. package/lib/handler/cache-handler.js +393 -0
  51. package/lib/handler/cache-revalidation-handler.js +124 -0
  52. package/lib/handler/decorator-handler.js +3 -0
  53. package/lib/handler/redirect-handler.js +45 -59
  54. package/lib/handler/retry-handler.js +68 -109
  55. package/lib/handler/unwrap-handler.js +96 -0
  56. package/lib/handler/wrap-handler.js +98 -0
  57. package/lib/interceptor/cache.js +350 -0
  58. package/lib/interceptor/dns.js +375 -0
  59. package/lib/interceptor/response-error.js +15 -7
  60. package/lib/mock/mock-agent.js +5 -8
  61. package/lib/mock/mock-client.js +7 -2
  62. package/lib/mock/mock-errors.js +3 -1
  63. package/lib/mock/mock-interceptor.js +8 -6
  64. package/lib/mock/mock-pool.js +7 -2
  65. package/lib/mock/mock-symbols.js +2 -1
  66. package/lib/mock/mock-utils.js +33 -5
  67. package/lib/util/cache.js +360 -0
  68. package/lib/util/timers.js +50 -6
  69. package/lib/web/cache/cache.js +25 -21
  70. package/lib/web/cache/cachestorage.js +3 -1
  71. package/lib/web/cookies/index.js +18 -5
  72. package/lib/web/cookies/parse.js +6 -1
  73. package/lib/web/eventsource/eventsource.js +2 -0
  74. package/lib/web/fetch/body.js +43 -39
  75. package/lib/web/fetch/constants.js +45 -29
  76. package/lib/web/fetch/data-url.js +2 -2
  77. package/lib/web/fetch/formdata-parser.js +84 -46
  78. package/lib/web/fetch/formdata.js +42 -20
  79. package/lib/web/fetch/headers.js +119 -85
  80. package/lib/web/fetch/index.js +69 -65
  81. package/lib/web/fetch/request.js +132 -55
  82. package/lib/web/fetch/response.js +81 -36
  83. package/lib/web/fetch/util.js +274 -103
  84. package/lib/web/fetch/webidl.js +54 -18
  85. package/lib/web/websocket/connection.js +92 -15
  86. package/lib/web/websocket/constants.js +69 -9
  87. package/lib/web/websocket/events.js +8 -2
  88. package/lib/web/websocket/receiver.js +20 -26
  89. package/lib/web/websocket/stream/websocketerror.js +83 -0
  90. package/lib/web/websocket/stream/websocketstream.js +485 -0
  91. package/lib/web/websocket/util.js +115 -10
  92. package/lib/web/websocket/websocket.js +47 -170
  93. package/package.json +15 -11
  94. package/types/agent.d.ts +1 -1
  95. package/types/cache-interceptor.d.ts +172 -0
  96. package/types/cookies.d.ts +2 -0
  97. package/types/dispatcher.d.ts +29 -4
  98. package/types/env-http-proxy-agent.d.ts +1 -1
  99. package/types/fetch.d.ts +9 -8
  100. package/types/handlers.d.ts +4 -4
  101. package/types/index.d.ts +3 -1
  102. package/types/interceptors.d.ts +18 -1
  103. package/types/mock-agent.d.ts +4 -1
  104. package/types/mock-client.d.ts +1 -1
  105. package/types/mock-pool.d.ts +1 -1
  106. package/types/proxy-agent.d.ts +1 -1
  107. package/types/readable.d.ts +10 -7
  108. package/types/retry-handler.d.ts +3 -3
  109. package/types/webidl.d.ts +30 -4
  110. package/types/websocket.d.ts +33 -0
  111. package/lib/mock/pluralizer.js +0 -29
  112. package/lib/web/cache/symbols.js +0 -5
  113. package/lib/web/fetch/symbols.js +0 -8
@@ -0,0 +1,124 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert')
4
+
5
+ /**
6
+ * This takes care of revalidation requests we send to the origin. If we get
7
+ * a response indicating that what we have is cached (via a HTTP 304), we can
8
+ * continue using the cached value. Otherwise, we'll receive the new response
9
+ * here, which we then just pass on to the next handler (most likely a
10
+ * CacheHandler). Note that this assumes the proper headers were already
11
+ * included in the request to tell the origin that we want to revalidate the
12
+ * response (i.e. if-modified-since).
13
+ *
14
+ * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-validation
15
+ *
16
+ * @implements {import('../../types/dispatcher.d.ts').default.DispatchHandler}
17
+ */
18
+ class CacheRevalidationHandler {
19
+ #successful = false
20
+
21
+ /**
22
+ * @type {((boolean, any) => void) | null}
23
+ */
24
+ #callback
25
+
26
+ /**
27
+ * @type {(import('../../types/dispatcher.d.ts').default.DispatchHandler)}
28
+ */
29
+ #handler
30
+
31
+ #context
32
+
33
+ /**
34
+ * @type {boolean}
35
+ */
36
+ #allowErrorStatusCodes
37
+
38
+ /**
39
+ * @param {(boolean) => void} callback Function to call if the cached value is valid
40
+ * @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
41
+ * @param {boolean} allowErrorStatusCodes
42
+ */
43
+ constructor (callback, handler, allowErrorStatusCodes) {
44
+ if (typeof callback !== 'function') {
45
+ throw new TypeError('callback must be a function')
46
+ }
47
+
48
+ this.#callback = callback
49
+ this.#handler = handler
50
+ this.#allowErrorStatusCodes = allowErrorStatusCodes
51
+ }
52
+
53
+ onRequestStart (_, context) {
54
+ this.#successful = false
55
+ this.#context = context
56
+ }
57
+
58
+ onRequestUpgrade (controller, statusCode, headers, socket) {
59
+ this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
60
+ }
61
+
62
+ onResponseStart (
63
+ controller,
64
+ statusCode,
65
+ headers,
66
+ statusMessage
67
+ ) {
68
+ assert(this.#callback != null)
69
+
70
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-a-validation-respo
71
+ // https://datatracker.ietf.org/doc/html/rfc5861#section-4
72
+ this.#successful = statusCode === 304 ||
73
+ (this.#allowErrorStatusCodes && statusCode >= 500 && statusCode <= 504)
74
+ this.#callback(this.#successful, this.#context)
75
+ this.#callback = null
76
+
77
+ if (this.#successful) {
78
+ return true
79
+ }
80
+
81
+ this.#handler.onRequestStart?.(controller, this.#context)
82
+ this.#handler.onResponseStart?.(
83
+ controller,
84
+ statusCode,
85
+ headers,
86
+ statusMessage
87
+ )
88
+ }
89
+
90
+ onResponseData (controller, chunk) {
91
+ if (this.#successful) {
92
+ return
93
+ }
94
+
95
+ return this.#handler.onResponseData?.(controller, chunk)
96
+ }
97
+
98
+ onResponseEnd (controller, trailers) {
99
+ if (this.#successful) {
100
+ return
101
+ }
102
+
103
+ this.#handler.onResponseEnd?.(controller, trailers)
104
+ }
105
+
106
+ onResponseError (controller, err) {
107
+ if (this.#successful) {
108
+ return
109
+ }
110
+
111
+ if (this.#callback) {
112
+ this.#callback(false)
113
+ this.#callback = null
114
+ }
115
+
116
+ if (typeof this.#handler.onResponseError === 'function') {
117
+ this.#handler.onResponseError(controller, err)
118
+ } else {
119
+ throw err
120
+ }
121
+ }
122
+ }
123
+
124
+ module.exports = CacheRevalidationHandler
@@ -2,6 +2,9 @@
2
2
 
3
3
  const assert = require('node:assert')
4
4
 
5
+ /**
6
+ * @deprecated
7
+ */
5
8
  module.exports = class DecoratorHandler {
6
9
  #handler
7
10
  #onCompleteCalled = false
@@ -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,16 +40,12 @@ class RedirectHandler {
38
40
  throw new InvalidArgumentError('maxRedirections must be a positive number')
39
41
  }
40
42
 
41
- util.validateHandler(handler, opts.method, opts.upgrade)
42
-
43
43
  this.dispatch = dispatch
44
44
  this.location = null
45
- this.abort = null
46
45
  this.opts = { ...opts, maxRedirections: 0 } // opts must be a copy
47
46
  this.maxRedirections = maxRedirections
48
47
  this.handler = handler
49
48
  this.history = []
50
- this.redirectionLimitReached = false
51
49
 
52
50
  if (util.isStream(this.opts.body)) {
53
51
  // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
@@ -75,7 +73,8 @@ class RedirectHandler {
75
73
  this.opts.body &&
76
74
  typeof this.opts.body !== 'string' &&
77
75
  !ArrayBuffer.isView(this.opts.body) &&
78
- util.isIterable(this.opts.body)
76
+ util.isIterable(this.opts.body) &&
77
+ !util.isFormDataLike(this.opts.body)
79
78
  ) {
80
79
  // TODO: Should we allow re-using iterable if !this.opts.idempotent
81
80
  // or through some other flag?
@@ -83,40 +82,51 @@ class RedirectHandler {
83
82
  }
84
83
  }
85
84
 
86
- onConnect (abort) {
87
- this.abort = abort
88
- this.handler.onConnect(abort, { history: this.history })
85
+ onRequestStart (controller, context) {
86
+ this.handler.onRequestStart?.(controller, { ...context, history: this.history })
89
87
  }
90
88
 
91
- onUpgrade (statusCode, headers, socket) {
92
- this.handler.onUpgrade(statusCode, headers, socket)
89
+ onRequestUpgrade (controller, statusCode, headers, socket) {
90
+ this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
93
91
  }
94
92
 
95
- onError (error) {
96
- this.handler.onError(error)
97
- }
98
-
99
- onHeaders (statusCode, headers, resume, statusText) {
100
- this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body)
101
- ? null
102
- : parseLocation(statusCode, headers)
103
-
93
+ onResponseStart (controller, statusCode, headers, statusMessage) {
104
94
  if (this.opts.throwOnMaxRedirect && this.history.length >= this.maxRedirections) {
105
- if (this.request) {
106
- this.request.abort(new Error('max redirects'))
95
+ throw new Error('max redirects')
96
+ }
97
+
98
+ // https://tools.ietf.org/html/rfc7231#section-6.4.2
99
+ // https://fetch.spec.whatwg.org/#http-redirect-fetch
100
+ // In case of HTTP 301 or 302 with POST, change the method to GET
101
+ if ((statusCode === 301 || statusCode === 302) && this.opts.method === 'POST') {
102
+ this.opts.method = 'GET'
103
+ if (util.isStream(this.opts.body)) {
104
+ util.destroy(this.opts.body.on('error', noop))
107
105
  }
106
+ this.opts.body = null
107
+ }
108
108
 
109
- this.redirectionLimitReached = true
110
- this.abort(new Error('max redirects'))
111
- return
109
+ // https://tools.ietf.org/html/rfc7231#section-6.4.4
110
+ // In case of HTTP 303, always replace method to be either HEAD or GET
111
+ if (statusCode === 303 && this.opts.method !== 'HEAD') {
112
+ this.opts.method = 'GET'
113
+ if (util.isStream(this.opts.body)) {
114
+ util.destroy(this.opts.body.on('error', noop))
115
+ }
116
+ this.opts.body = null
112
117
  }
113
118
 
119
+ this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body) || redirectableStatusCodes.indexOf(statusCode) === -1
120
+ ? null
121
+ : headers.location
122
+
114
123
  if (this.opts.origin) {
115
124
  this.history.push(new URL(this.opts.path, this.opts.origin))
116
125
  }
117
126
 
118
127
  if (!this.location) {
119
- return this.handler.onHeaders(statusCode, headers, resume, statusText)
128
+ this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
129
+ return
120
130
  }
121
131
 
122
132
  const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
@@ -130,23 +140,16 @@ class RedirectHandler {
130
140
  this.opts.origin = origin
131
141
  this.opts.maxRedirections = 0
132
142
  this.opts.query = null
133
-
134
- // https://tools.ietf.org/html/rfc7231#section-6.4.4
135
- // In case of HTTP 303, always replace method to be either HEAD or GET
136
- if (statusCode === 303 && this.opts.method !== 'HEAD') {
137
- this.opts.method = 'GET'
138
- this.opts.body = null
139
- }
140
143
  }
141
144
 
142
- onData (chunk) {
145
+ onResponseData (controller, chunk) {
143
146
  if (this.location) {
144
147
  /*
145
148
  https://tools.ietf.org/html/rfc7231#section-6.4
146
149
 
147
150
  TLDR: undici always ignores 3xx response bodies.
148
151
 
149
- Redirection is used to serve the requested resource from another URL, so it is assumes that
152
+ Redirection is used to serve the requested resource from another URL, so it assumes that
150
153
  no body is generated (and thus can be ignored). Even though generating a body is not prohibited.
151
154
 
152
155
  For status 301, 302, 303, 307 and 308 (the latter from RFC 7238), the specs mention that the body usually
@@ -159,11 +162,11 @@ class RedirectHandler {
159
162
  servers and browsers implementors, we ignore the body as there is no specified way to eventually parse it.
160
163
  */
161
164
  } else {
162
- return this.handler.onData(chunk)
165
+ this.handler.onResponseData?.(controller, chunk)
163
166
  }
164
167
  }
165
168
 
166
- onComplete (trailers) {
169
+ onResponseEnd (controller, trailers) {
167
170
  if (this.location) {
168
171
  /*
169
172
  https://tools.ietf.org/html/rfc7231#section-6.4
@@ -173,32 +176,14 @@ class RedirectHandler {
173
176
 
174
177
  See comment on onData method above for more detailed information.
175
178
  */
176
-
177
- this.location = null
178
- this.abort = null
179
-
180
179
  this.dispatch(this.opts, this)
181
180
  } else {
182
- this.handler.onComplete(trailers)
181
+ this.handler.onResponseEnd(controller, trailers)
183
182
  }
184
183
  }
185
184
 
186
- onBodySent (chunk) {
187
- if (this.handler.onBodySent) {
188
- this.handler.onBodySent(chunk)
189
- }
190
- }
191
- }
192
-
193
- function parseLocation (statusCode, headers) {
194
- if (redirectableStatusCodes.indexOf(statusCode) === -1) {
195
- return null
196
- }
197
-
198
- for (let i = 0; i < headers.length; i += 2) {
199
- if (headers[i].length === 8 && util.headerNameToString(headers[i]) === 'location') {
200
- return headers[i + 1]
201
- }
185
+ onResponseError (controller, error) {
186
+ this.handler.onResponseError?.(controller, error)
202
187
  }
203
188
  }
204
189
 
@@ -227,9 +212,10 @@ function cleanRequestHeaders (headers, removeContent, unknownOrigin) {
227
212
  }
228
213
  }
229
214
  } else if (headers && typeof headers === 'object') {
230
- for (const key of Object.keys(headers)) {
215
+ const entries = typeof headers[Symbol.iterator] === 'function' ? headers : Object.entries(headers)
216
+ for (const [key, value] of entries) {
231
217
  if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) {
232
- ret.push(key, headers[key])
218
+ ret.push(key, value)
233
219
  }
234
220
  }
235
221
  } else {
@@ -3,9 +3,9 @@ const assert = require('node:assert')
3
3
 
4
4
  const { kRetryHandlerDefaultRetry } = require('../core/symbols')
5
5
  const { RequestRetryError } = require('../core/errors')
6
+ const WrapHandler = require('./wrap-handler')
6
7
  const {
7
8
  isDisturbed,
8
- parseHeaders,
9
9
  parseRangeHeader,
10
10
  wrapRequestBody
11
11
  } = require('../core/util')
@@ -16,7 +16,7 @@ function calculateRetryAfterHeader (retryAfter) {
16
16
  }
17
17
 
18
18
  class RetryHandler {
19
- constructor (opts, handlers) {
19
+ constructor (opts, { dispatch, handler }) {
20
20
  const { retryOptions, ...dispatchOpts } = opts
21
21
  const {
22
22
  // Retry scoped
@@ -32,11 +32,9 @@ class RetryHandler {
32
32
  statusCodes
33
33
  } = retryOptions ?? {}
34
34
 
35
- this.dispatch = handlers.dispatch
36
- this.handler = handlers.handler
35
+ this.dispatch = dispatch
36
+ this.handler = WrapHandler.wrap(handler)
37
37
  this.opts = { ...dispatchOpts, body: wrapRequestBody(opts.body) }
38
- this.abort = null
39
- this.aborted = false
40
38
  this.retryOpts = {
41
39
  retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry],
42
40
  retryAfter: retryAfter ?? true,
@@ -64,44 +62,20 @@ class RetryHandler {
64
62
 
65
63
  this.retryCount = 0
66
64
  this.retryCountCheckpoint = 0
65
+ this.headersSent = false
67
66
  this.start = 0
68
67
  this.end = null
69
68
  this.etag = null
70
- 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
- }
82
-
83
- onRequestSent () {
84
- if (this.handler.onRequestSent) {
85
- this.handler.onRequestSent()
86
- }
87
- }
88
-
89
- onUpgrade (statusCode, headers, socket) {
90
- if (this.handler.onUpgrade) {
91
- this.handler.onUpgrade(statusCode, headers, socket)
92
- }
93
69
  }
94
70
 
95
- onConnect (abort) {
96
- if (this.aborted) {
97
- abort(this.reason)
98
- } else {
99
- this.abort = abort
71
+ onRequestStart (controller, context) {
72
+ if (!this.headersSent) {
73
+ this.handler.onRequestStart?.(controller, context)
100
74
  }
101
75
  }
102
76
 
103
- onBodySent (chunk) {
104
- if (this.handler.onBodySent) return this.handler.onBodySent(chunk)
77
+ onRequestUpgrade (controller, statusCode, headers, socket) {
78
+ this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
105
79
  }
106
80
 
107
81
  static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) {
@@ -159,83 +133,68 @@ class RetryHandler {
159
133
  ? Math.min(retryAfterHeader, maxTimeout)
160
134
  : Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout)
161
135
 
162
- setTimeout(() => cb(null), retryTimeout)
136
+ setTimeout(() => cb(null), retryTimeout).unref()
163
137
  }
164
138
 
165
- onHeaders (statusCode, rawHeaders, resume, statusMessage) {
166
- const headers = parseHeaders(rawHeaders)
167
-
139
+ onResponseStart (controller, statusCode, headers, statusMessage) {
168
140
  this.retryCount += 1
169
141
 
170
142
  if (statusCode >= 300) {
171
143
  if (this.retryOpts.statusCodes.includes(statusCode) === false) {
172
- return this.handler.onHeaders(
144
+ this.headersSent = true
145
+ this.handler.onResponseStart?.(
146
+ controller,
173
147
  statusCode,
174
- rawHeaders,
175
- resume,
148
+ headers,
176
149
  statusMessage
177
150
  )
151
+ return
178
152
  } else {
179
- this.abort(
180
- new RequestRetryError('Request failed', statusCode, {
181
- headers,
182
- data: {
183
- count: this.retryCount
184
- }
185
- })
186
- )
187
- return false
153
+ throw new RequestRetryError('Request failed', statusCode, {
154
+ headers,
155
+ data: {
156
+ count: this.retryCount
157
+ }
158
+ })
188
159
  }
189
160
  }
190
161
 
191
162
  // Checkpoint for resume from where we left it
192
- if (this.resume != null) {
193
- this.resume = null
194
-
163
+ if (this.headersSent) {
195
164
  // Only Partial Content 206 supposed to provide Content-Range,
196
165
  // any other status code that partially consumed the payload
197
- // should not be retry because it would result in downstream
198
- // wrongly concatanete multiple responses.
166
+ // should not be retried because it would result in downstream
167
+ // wrongly concatenate multiple responses.
199
168
  if (statusCode !== 206 && (this.start > 0 || statusCode !== 200)) {
200
- this.abort(
201
- new RequestRetryError('server does not support the range header and the payload was partially consumed', statusCode, {
202
- headers,
203
- data: { count: this.retryCount }
204
- })
205
- )
206
- return false
169
+ throw new RequestRetryError('server does not support the range header and the payload was partially consumed', statusCode, {
170
+ headers,
171
+ data: { count: this.retryCount }
172
+ })
207
173
  }
208
174
 
209
175
  const contentRange = parseRangeHeader(headers['content-range'])
210
176
  // If no content range
211
177
  if (!contentRange) {
212
- this.abort(
213
- new RequestRetryError('Content-Range mismatch', statusCode, {
214
- headers,
215
- data: { count: this.retryCount }
216
- })
217
- )
218
- return false
178
+ throw new RequestRetryError('Content-Range mismatch', statusCode, {
179
+ headers,
180
+ data: { count: this.retryCount }
181
+ })
219
182
  }
220
183
 
221
184
  // Let's start with a weak etag check
222
185
  if (this.etag != null && this.etag !== headers.etag) {
223
- this.abort(
224
- new RequestRetryError('ETag mismatch', statusCode, {
225
- headers,
226
- data: { count: this.retryCount }
227
- })
228
- )
229
- return false
186
+ throw new RequestRetryError('ETag mismatch', statusCode, {
187
+ headers,
188
+ data: { count: this.retryCount }
189
+ })
230
190
  }
231
191
 
232
- const { start, size, end = size } = contentRange
192
+ const { start, size, end = size ? size - 1 : null } = contentRange
233
193
 
234
194
  assert(this.start === start, 'content-range mismatch')
235
195
  assert(this.end == null || this.end === end, 'content-range mismatch')
236
196
 
237
- this.resume = resume
238
- return true
197
+ return
239
198
  }
240
199
 
241
200
  if (this.end == null) {
@@ -244,15 +203,17 @@ class RetryHandler {
244
203
  const range = parseRangeHeader(headers['content-range'])
245
204
 
246
205
  if (range == null) {
247
- return this.handler.onHeaders(
206
+ this.headersSent = true
207
+ this.handler.onResponseStart?.(
208
+ controller,
248
209
  statusCode,
249
- rawHeaders,
250
- resume,
210
+ headers,
251
211
  statusMessage
252
212
  )
213
+ return
253
214
  }
254
215
 
255
- const { start, size, end = size } = range
216
+ const { start, size, end = size ? size - 1 : null } = range
256
217
  assert(
257
218
  start != null && Number.isFinite(start),
258
219
  'content-range mismatch'
@@ -266,7 +227,7 @@ class RetryHandler {
266
227
  // We make our best to checkpoint the body for further range headers
267
228
  if (this.end == null) {
268
229
  const contentLength = headers['content-length']
269
- this.end = contentLength != null ? Number(contentLength) : null
230
+ this.end = contentLength != null ? Number(contentLength) - 1 : null
270
231
  }
271
232
 
272
233
  assert(Number.isFinite(this.start))
@@ -275,7 +236,7 @@ class RetryHandler {
275
236
  'invalid content-length'
276
237
  )
277
238
 
278
- this.resume = resume
239
+ this.resume = true
279
240
  this.etag = headers.etag != null ? headers.etag : null
280
241
 
281
242
  // Weak etags are not useful for comparison nor cache
@@ -289,38 +250,36 @@ class RetryHandler {
289
250
  this.etag = null
290
251
  }
291
252
 
292
- return this.handler.onHeaders(
253
+ this.headersSent = true
254
+ this.handler.onResponseStart?.(
255
+ controller,
293
256
  statusCode,
294
- rawHeaders,
295
- resume,
257
+ headers,
296
258
  statusMessage
297
259
  )
260
+ } else {
261
+ throw new RequestRetryError('Request failed', statusCode, {
262
+ headers,
263
+ data: { count: this.retryCount }
264
+ })
298
265
  }
299
-
300
- const err = new RequestRetryError('Request failed', statusCode, {
301
- headers,
302
- data: { count: this.retryCount }
303
- })
304
-
305
- this.abort(err)
306
-
307
- return false
308
266
  }
309
267
 
310
- onData (chunk) {
268
+ onResponseData (controller, chunk) {
311
269
  this.start += chunk.length
312
270
 
313
- return this.handler.onData(chunk)
271
+ this.handler.onResponseData?.(controller, chunk)
314
272
  }
315
273
 
316
- onComplete (rawTrailers) {
274
+ onResponseEnd (controller, trailers) {
317
275
  this.retryCount = 0
318
- return this.handler.onComplete(rawTrailers)
276
+ return this.handler.onResponseEnd?.(controller, trailers)
319
277
  }
320
278
 
321
- onError (err) {
322
- if (this.aborted || isDisturbed(this.opts.body)) {
323
- return this.handler.onError(err)
279
+ onResponseError (controller, err) {
280
+ if (!controller || controller.aborted || isDisturbed(this.opts.body)) {
281
+ this.handler.onResponseError?.(controller, err)
282
+ return
324
283
  }
325
284
 
326
285
  // We reconcile in case of a mix between network errors
@@ -349,8 +308,8 @@ class RetryHandler {
349
308
  * @returns
350
309
  */
351
310
  function onRetry (err) {
352
- if (err != null || this.aborted || isDisturbed(this.opts.body)) {
353
- return this.handler.onError(err)
311
+ if (err != null || controller?.aborted || isDisturbed(this.opts.body)) {
312
+ return this.handler.onResponseError?.(controller, err)
354
313
  }
355
314
 
356
315
  if (this.start !== 0) {
@@ -374,7 +333,7 @@ class RetryHandler {
374
333
  this.retryCountCheckpoint = this.retryCount
375
334
  this.dispatch(this.opts, this)
376
335
  } catch (err) {
377
- this.handler.onError(err)
336
+ this.handler.onResponseError?.(controller, err)
378
337
  }
379
338
  }
380
339
  }