undici 4.11.0 → 4.12.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.
package/README.md CHANGED
@@ -203,13 +203,13 @@ not support or does not fully implement.
203
203
  * https://fetch.spec.whatwg.org/#garbage-collection
204
204
 
205
205
  The [Fetch Standard](https://fetch.spec.whatwg.org) allows users to skip consuming the response body by relying on
206
- [garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources. Undici does the same. However,
207
- garbage collection in Node is less aggressive and deterministic (due to the lack
208
- of clear idle periods that browser have through the rendering refresh rate)
209
- which means that leaving the release of connection resources to the garbage collector
210
- can lead to excessive connection usage, reduced performance (due to less connection re-use),
211
- and even stalls or deadlocks when running out of connections. Therefore, it is highly
212
- recommended to always either consume or cancel the response body.
206
+ [garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources. Undici does not do the same. Therefore, it is important to always either consume or cancel the response body.
207
+
208
+ Garbage collection in Node is less aggressive and deterministic
209
+ (due to the lack of clear idle periods that browser have through the rendering refresh rate)
210
+ which means that leaving the release of connection resources to the garbage collector can lead
211
+ to excessive connection usage, reduced performance (due to less connection re-use), and even
212
+ stalls or deadlocks when running out of connections.
213
213
 
214
214
  ```js
215
215
  // Do
@@ -289,7 +289,7 @@ pipeline requests, without checking whether the connection is persistent.
289
289
  Hence, automatic fallback to HTTP/1.0 or HTTP/1.1 without pipelining is
290
290
  not supported.
291
291
 
292
- Undici will immediately pipeline when retrying requests afters a failed
292
+ Undici will immediately pipeline when retrying requests after a failed
293
293
  connection. However, Undici will not retry the first remaining requests in
294
294
  the prior pipeline and instead error the corresponding callback/promise/stream.
295
295
 
@@ -56,8 +56,8 @@ module.exports = class BodyReadable extends Readable {
56
56
  }
57
57
 
58
58
  emit (ev, ...args) {
59
- // Waiting for: https://github.com/nodejs/node/pull/39589
60
59
  if (ev === 'data') {
60
+ // Node < 16.7
61
61
  this._readableState.dataEmitted = true
62
62
  } else if (ev === 'error') {
63
63
  // Node < 16
@@ -93,17 +93,16 @@ module.exports = class BodyReadable extends Readable {
93
93
  }
94
94
 
95
95
  push (chunk) {
96
- if (this[kConsume] && chunk !== null && !this[kReading]) {
96
+ if (this[kConsume] && chunk !== null) {
97
97
  consumePush(this[kConsume], chunk)
98
- return true
99
- } else {
100
- return super.push(chunk)
98
+ return this[kReading] ? super.push(chunk) : true
101
99
  }
100
+ return super.push(chunk)
102
101
  }
103
102
 
104
103
  // https://fetch.spec.whatwg.org/#dom-body-text
105
104
  async text () {
106
- return toUSVString(await consume(this, 'text'))
105
+ return consume(this, 'text')
107
106
  }
108
107
 
109
108
  // https://fetch.spec.whatwg.org/#dom-body-json
@@ -145,7 +144,7 @@ module.exports = class BodyReadable extends Readable {
145
144
  return this[kBody]
146
145
  }
147
146
 
148
- async dump(opts) {
147
+ async dump (opts) {
149
148
  let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144
150
149
  try {
151
150
  for await (const chunk of this) {
@@ -233,7 +232,7 @@ function consumeEnd (consume) {
233
232
 
234
233
  try {
235
234
  if (type === 'text') {
236
- resolve(Buffer.concat(body))
235
+ resolve(toUSVString(Buffer.concat(body)))
237
236
  } else if (type === 'json') {
238
237
  resolve(JSON.parse(Buffer.concat(body)))
239
238
  } else if (type === 'arrayBuffer') {
@@ -275,8 +274,10 @@ function consumeFinish (consume, err) {
275
274
  consume.resolve()
276
275
  }
277
276
 
278
- consume.reject = null
277
+ consume.type = null
278
+ consume.stream = null
279
279
  consume.resolve = null
280
- consume.decoder = null
280
+ consume.reject = null
281
+ consume.length = 0
281
282
  consume.body = null
282
283
  }
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const {
4
- BalancedPoolMissingUpstreamError,
4
+ BalancedPoolMissingUpstreamError
5
5
  } = require('./core/errors')
6
6
  const {
7
7
  PoolBase,
@@ -9,8 +9,8 @@ const {
9
9
  kNeedDrain,
10
10
  kAddClient,
11
11
  kRemoveClient,
12
- kDispatch,
13
- } = require('./pool-base')
12
+ kDispatch
13
+ } = require('./pool-base')
14
14
  const Pool = require('./pool')
15
15
  const { kUrl } = require('./core/symbols')
16
16
 
@@ -73,7 +73,7 @@ class BalancedPool extends PoolBase {
73
73
  throw new BalancedPoolMissingUpstreamError()
74
74
  }
75
75
 
76
- let dispatcher = this[kClients].find(dispatcher => (
76
+ const dispatcher = this[kClients].find(dispatcher => (
77
77
  !dispatcher[kNeedDrain] &&
78
78
  dispatcher.closed !== true &&
79
79
  dispatcher.destroyed !== true
package/lib/core/util.js CHANGED
@@ -150,7 +150,7 @@ function isDestroyed (stream) {
150
150
  return !stream || !!(stream.destroyed || stream[kDestroyed])
151
151
  }
152
152
 
153
- function isAborted (stream) {
153
+ function isReadableAborted (stream) {
154
154
  const state = stream && stream._readableState
155
155
  return isDestroyed(stream) && state && !state.endEmitted
156
156
  }
@@ -244,15 +244,28 @@ function validateHandler (handler, method, upgrade) {
244
244
  // A body is disturbed if it has been read from and it cannot
245
245
  // be re-used without losing state or data.
246
246
  function isDisturbed (body) {
247
- const state = body && body._readableState
248
247
  return !!(body && (
249
- (stream.isDisturbed && stream.isDisturbed(body)) ||
250
- body[kBodyUsed] ||
251
- body.readableDidRead || (state && state.dataEmitted) ||
252
- isAborted(body)
248
+ stream.isDisturbed
249
+ ? stream.isDisturbed(body) || body[kBodyUsed] // TODO (fix): Why is body[kBodyUsed] needed?
250
+ : body[kBodyUsed] ||
251
+ body.readableDidRead ||
252
+ (body._readableState && body._readableState.dataEmitted) ||
253
+ isReadableAborted(body)
253
254
  ))
254
255
  }
255
256
 
257
+ function isErrored (body) {
258
+ return !!(body && (
259
+ stream.isErrored
260
+ ? stream.isErrored(body)
261
+ : /state: 'errored'/.test(nodeUtil.inspect(body)
262
+ )))
263
+ }
264
+
265
+ function isReadable (body) {
266
+ return !!(body && /state: 'readable'/.test(nodeUtil.inspect(body)))
267
+ }
268
+
256
269
  function getSocketInfo (socket) {
257
270
  return {
258
271
  localAddress: socket.localAddress,
@@ -310,8 +323,10 @@ module.exports = {
310
323
  kEnumerableProperty,
311
324
  nop,
312
325
  isDisturbed,
326
+ isErrored,
327
+ isReadable,
313
328
  toUSVString: nodeUtil.toUSVString || ((val) => `${val}`),
314
- isAborted,
329
+ isReadableAborted,
315
330
  isBlobLike,
316
331
  parseOrigin,
317
332
  parseURL,
package/lib/fetch/body.js CHANGED
@@ -9,6 +9,7 @@ const { kBodyUsed } = require('../core/symbols')
9
9
  const assert = require('assert')
10
10
  const nodeUtil = require('util')
11
11
  const { NotSupportedError } = require('../core/errors')
12
+ const { isErrored } = require('../core/util')
12
13
 
13
14
  let ReadableStream
14
15
 
@@ -169,7 +170,7 @@ function extractBody (object, keepalive = false) {
169
170
  }
170
171
 
171
172
  // 8. If action is non-null, then run these steps in in parallel:
172
- if (action !== null) {
173
+ if (action != null) {
173
174
  // Run action.
174
175
  let iterator
175
176
  stream = new ReadableStream({
@@ -187,7 +188,7 @@ function extractBody (object, keepalive = false) {
187
188
  // Whenever one or more bytes are available and stream is not errored,
188
189
  // enqueue a Uint8Array wrapping an ArrayBuffer containing the available
189
190
  // bytes into stream.
190
- if (!/state: 'errored'/.test(nodeUtil.inspect(stream))) {
191
+ if (!isErrored(stream)) {
191
192
  controller.enqueue(new Uint8Array(value))
192
193
  }
193
194
  }
@@ -348,19 +349,12 @@ const properties = {
348
349
  }
349
350
  }
350
351
 
351
- function cancelBody (body, reason) {
352
- if (body.stream && !/state: 'errored'/.test(nodeUtil.inspect(body.stream))) {
353
- body.stream.cancel(reason)
354
- }
355
- }
356
-
357
352
  function mixinBody (prototype) {
358
353
  Object.assign(prototype, methods)
359
354
  Object.defineProperties(prototype, properties)
360
355
  }
361
356
 
362
357
  module.exports = {
363
- cancelBody,
364
358
  extractBody,
365
359
  safelyExtractBody,
366
360
  cloneBody,
package/lib/fetch/file.js CHANGED
@@ -118,6 +118,7 @@ class FileLike {
118
118
  this[kState] = {
119
119
  blobLike,
120
120
  name: n,
121
+ type: t,
121
122
  lastModified: d
122
123
  }
123
124
  }
@@ -153,7 +153,7 @@ class Headers {
153
153
  constructor (...args) {
154
154
  if (
155
155
  args[0] !== undefined &&
156
- !(typeof args[0] === 'object' && args[0] !== null) &&
156
+ !(typeof args[0] === 'object' && args[0] != null) &&
157
157
  !Array.isArray(args[0])
158
158
  ) {
159
159
  throw new TypeError(
@@ -24,7 +24,7 @@ const {
24
24
  requestCurrentURL,
25
25
  setRequestReferrerPolicyOnRedirect,
26
26
  tryUpgradeRequestToAPotentiallyTrustworthyURL,
27
- makeTimingInfo,
27
+ createOpaqueTimingInfo,
28
28
  appendFetchMetadata,
29
29
  corsCheck,
30
30
  crossOriginResourcePolicyCheck,
@@ -34,7 +34,7 @@ const {
34
34
  const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
35
35
  const { AbortError } = require('../core/errors')
36
36
  const assert = require('assert')
37
- const { safelyExtractBody, cancelBody } = require('./body')
37
+ const { safelyExtractBody } = require('./body')
38
38
  const {
39
39
  redirectStatus,
40
40
  nullBodyStatus,
@@ -44,14 +44,32 @@ const {
44
44
  } = require('./constants')
45
45
  const { kHeadersList } = require('../core/symbols')
46
46
  const EE = require('events')
47
- const { PassThrough, pipeline, compose } = require('stream')
47
+ const { PassThrough, pipeline } = require('stream')
48
+ const { isErrored, isReadable } = require('../core/util')
48
49
 
49
50
  let ReadableStream
50
51
 
51
- // https://fetch.spec.whatwg.org/#garbage-collection
52
- const registry = new FinalizationRegistry((abort) => {
53
- abort()
54
- })
52
+ class Fetch extends EE {
53
+ constructor (dispatcher) {
54
+ super()
55
+
56
+ this.dispatcher = dispatcher
57
+ this.terminated = null
58
+ this.connection = null
59
+ this.dump = false
60
+ }
61
+
62
+ terminate ({ reason, aborted } = {}) {
63
+ if (this.terminated) {
64
+ return
65
+ }
66
+ this.terminated = { aborted, reason }
67
+
68
+ this.connection?.destroy(reason)
69
+
70
+ this.emit('terminated', reason)
71
+ }
72
+ }
55
73
 
56
74
  // https://fetch.spec.whatwg.org/#fetch-method
57
75
  async function fetch (...args) {
@@ -73,26 +91,7 @@ async function fetch (...args) {
73
91
  const resource = args[0]
74
92
  const init = args.length >= 1 ? args[1] ?? {} : {}
75
93
 
76
- const context = Object.assign(new EE(), {
77
- dispatcher: this,
78
- terminated: false,
79
- connection: null,
80
- dump: false,
81
- terminate ({ reason, aborted } = {}) {
82
- if (this.terminated) {
83
- return
84
- }
85
-
86
- if (this.connection) {
87
- this.connection.destroy()
88
- this.connection = null
89
- }
90
-
91
- this.terminated = { aborted }
92
-
93
- this.emit('terminated', reason)
94
- }
95
- })
94
+ const context = new Fetch(this)
96
95
 
97
96
  // 1. Let p be a new promise.
98
97
  const p = createDeferredPromise()
@@ -154,7 +153,7 @@ async function fetch (...args) {
154
153
  const handleFetchDone = (response) =>
155
154
  finalizeAndReportTiming(response, 'fetch')
156
155
 
157
- // 12. Fetch request with processResponseDone set to handleFetchDone,
156
+ // 12. Fetch request with processResponseEndOfBody set to handleFetchDone,
158
157
  // and processResponse given response being these substeps:
159
158
  const processResponse = (response) => {
160
159
  // 1. If locallyAborted is true, terminate these substeps.
@@ -194,7 +193,7 @@ async function fetch (...args) {
194
193
  fetching
195
194
  .call(context, {
196
195
  request,
197
- processResponseDone: handleFetchDone,
196
+ processResponseEndOfBody: handleFetchDone,
198
197
  processResponse
199
198
  })
200
199
  .catch((err) => {
@@ -227,11 +226,9 @@ function finalizeAndReportTiming (response, initiatorType = 'other') {
227
226
 
228
227
  // 6. If response’s timing allow passed flag is not set, then:
229
228
  if (!timingInfo.timingAllowPassed) {
230
- // 1. Set timingInfo to a new fetch timing info whose start time and
231
- // post-redirect start time are timingInfo’s start time.
232
- timingInfo = makeTimingInfo({
233
- startTime: timingInfo.startTime,
234
- postRedirectStartTime: timingInfo.postRedirectStartTime
229
+ // 1. Set timingInfo to a the result of creating an opaque timing info for timingInfo.
230
+ timingInfo = createOpaqueTimingInfo({
231
+ startTime: timingInfo.startTime
235
232
  })
236
233
 
237
234
  // 2. Set cacheState to the empty string.
@@ -266,8 +263,6 @@ function markResourceTiming () {
266
263
 
267
264
  // https://fetch.spec.whatwg.org/#abort-fetch
268
265
  function abortFetch (p, request, responseObject) {
269
- const context = this
270
-
271
266
  // 1. Let error be an "AbortError" DOMException.
272
267
  const error = new AbortError()
273
268
 
@@ -276,8 +271,14 @@ function abortFetch (p, request, responseObject) {
276
271
 
277
272
  // 3. If request’s body is not null and is readable, then cancel request’s
278
273
  // body with error.
279
- if (request.body !== null) {
280
- cancelBody(request.body, error)
274
+ if (request.body != null && isReadable(request.body?.stream)) {
275
+ request.body.stream.cancel(error).catch((err) => {
276
+ if (err.code === 'ERR_INVALID_STATE') {
277
+ // Node bug?
278
+ return
279
+ }
280
+ throw err
281
+ })
281
282
  }
282
283
 
283
284
  // 4. If responseObject is null, then return.
@@ -290,13 +291,27 @@ function abortFetch (p, request, responseObject) {
290
291
 
291
292
  // 6. If response’s body is not null and is readable, then error response’s
292
293
  // body with error.
293
- if (response.body != null) {
294
- context.connection.destroy(error)
294
+ if (response.body != null && isReadable(response.body?.stream)) {
295
+ response.body.stream.cancel(error).catch((err) => {
296
+ if (err.code === 'ERR_INVALID_STATE') {
297
+ // Node bug?
298
+ return
299
+ }
300
+ throw err
301
+ })
295
302
  }
296
303
  }
297
304
 
298
305
  // https://fetch.spec.whatwg.org/#fetching
299
- function fetching ({ request, processResponse, processResponseDone }) {
306
+ function fetching ({
307
+ request,
308
+ processRequestBodyChunkLength,
309
+ processRequestEndOfBody,
310
+ processResponse,
311
+ processResponseEndOfBody,
312
+ processResponseConsumeBody,
313
+ useParallelQueue = false,
314
+ }) {
300
315
  // 1. Let taskDestination be null.
301
316
  let taskDestination = null
302
317
 
@@ -304,7 +319,7 @@ function fetching ({ request, processResponse, processResponseDone }) {
304
319
  let crossOriginIsolatedCapability = false
305
320
 
306
321
  // 3. If request’s client is non-null, then:
307
- if (request.client !== null) {
322
+ if (request.client != null) {
308
323
  // 1. Set taskDestination to request’s client’s global object.
309
324
  taskDestination = request.client.globalObject
310
325
 
@@ -322,26 +337,28 @@ function fetching ({ request, processResponse, processResponseDone }) {
322
337
  // post-redirect start time are the coarsened shared current time given
323
338
  // crossOriginIsolatedCapability.
324
339
  const currenTime = coarsenedSharedCurrentTime(crossOriginIsolatedCapability)
325
- const timingInfo = makeTimingInfo({
326
- startTime: currenTime,
327
- postRedirectStartTime: currenTime
340
+ const timingInfo = createOpaqueTimingInfo({
341
+ startTime: currenTime
328
342
  })
329
343
 
330
- // 6. Let fetchParams be a new fetch params whose request is request, timing
331
- // info is timingInfo, process request body is processRequestBody,
332
- // process request end-of-body is processRequestEndOfBody, process response
333
- // is processResponse, process response end-of-body is
334
- // processResponseEndOfBody, process response done is processResponseDone,
335
- // task destination is taskDestination, and cross-origin isolated capability
336
- // is crossOriginIsolatedCapability.
344
+ // 6. Let fetchParams be a new fetch params whose
345
+ // request is request,
346
+ // timing info is timingInfo,
347
+ // process request body chunk length is processRequestBodyChunkLength,
348
+ // process request end-of-body is processRequestEndOfBody,
349
+ // process response is processResponse,
350
+ // process response consume body is processResponseConsumeBody,
351
+ // process response end-of-body is processResponseEndOfBody,
352
+ // task destination is taskDestination,
353
+ // and cross-origin isolated capability is crossOriginIsolatedCapability.
337
354
  const fetchParams = {
338
355
  request,
339
356
  timingInfo,
340
- processRequestBody: null,
341
- processRequestEndOfBody: null,
357
+ processRequestBodyChunkLength,
358
+ processRequestEndOfBody,
342
359
  processResponse,
343
- processResponseEndOfBody: null,
344
- processResponseDone,
360
+ processResponseConsumeBody,
361
+ processResponseEndOfBody,
345
362
  taskDestination,
346
363
  crossOriginIsolatedCapability
347
364
  }
@@ -374,7 +391,7 @@ function fetching ({ request, processResponse, processResponseDone }) {
374
391
  if (request.policyContainer === 'client') {
375
392
  // 1. If request’s client is non-null, then set request’s policy
376
393
  // container to a clone of request’s client’s policy container. [HTML]
377
- if (request.client !== null) {
394
+ if (request.client != null) {
378
395
  request.policyContainer = clonePolicyContainer(
379
396
  request.client.policyContainer
380
397
  )
@@ -649,7 +666,7 @@ async function mainFetch (fetchParams, recursive = false) {
649
666
  nullBodyStatus.includes(internalResponse.status))
650
667
  ) {
651
668
  internalResponse.body = null
652
- context.connection.dump = true
669
+ context.dump = true
653
670
  }
654
671
 
655
672
  // 20. If request’s integrity metadata is not the empty string, then:
@@ -659,10 +676,9 @@ async function mainFetch (fetchParams, recursive = false) {
659
676
  const processBodyError = (reason) =>
660
677
  fetchFinale(fetchParams, makeNetworkError(reason))
661
678
 
662
- // 2. If request’s response tainting is "opaque", response is a network
663
- // error, or response’s body is null, then run processBodyError and abort
664
- // these steps.
665
- if (request.responseTainting === 'opaque' && response.status === 0) {
679
+ // 2. If request’s response tainting is "opaque", or response’s body is null,
680
+ // then run processBodyError and abort these steps.
681
+ if (request.responseTainting === 'opaque' || response.body == null) {
666
682
  processBodyError(response.error)
667
683
  return
668
684
  }
@@ -704,7 +720,7 @@ function finalizeResponse (fetchParams, response) {
704
720
  // 2, If fetchParams’s process response done is not null, then queue a fetch
705
721
  // task to run fetchParams’s process response done given response, with
706
722
  // fetchParams’s task destination.
707
- if (fetchParams.processResponseDone !== null) {
723
+ if (fetchParams.processResponseDone != null) {
708
724
  fetchParams.processResponseDone(response)
709
725
  }
710
726
  }
@@ -716,17 +732,17 @@ function fetchFinale (fetchParams, response) {
716
732
  // 1. If fetchParams’s process response is non-null,
717
733
  // then queue a fetch task to run fetchParams’s process response
718
734
  // given response, with fetchParams’s task destination.
719
- if (fetchParams.processResponse !== null) {
735
+ if (fetchParams.processResponse != null) {
720
736
  fetchParams.processResponse(response)
721
737
  }
722
738
 
723
- // 2. If fetchParams’s process response end-of-body is non-null, then:.
739
+ // 2. If fetchParams’s process response consume is non-null, then:.
724
740
  // TODO
725
741
  // 1. Let processBody given nullOrBytes be this step: run fetchParams’s
726
- // process response end-of-body given response and nullOrBytes.on.
742
+ // process response consume given response and nullOrBytes.on.
727
743
  // TODO
728
744
  // 2. Let processBodyError be this step: run fetchParams’s process
729
- // response end-of-body given response and failure.on.
745
+ // response consume given response and failure.on.
730
746
  // TODO
731
747
  // 3. If response’s body is null, then queue a fetch task to run
732
748
  // processBody given null, with fetchParams’s task destination.on.
@@ -915,7 +931,7 @@ async function httpRedirectFetch (fetchParams, response) {
915
931
  // and request’s body’s source is null, then return a network error.
916
932
  if (
917
933
  actualResponse.status !== 303 &&
918
- request.body !== null &&
934
+ request.body != null &&
919
935
  request.body.source == null
920
936
  ) {
921
937
  return makeNetworkError()
@@ -953,7 +969,7 @@ async function httpRedirectFetch (fetchParams, response) {
953
969
 
954
970
  // 14. If request’s body is non-null, then set request’s body to the first return
955
971
  // value of safely extracting request’s body’s source.
956
- if (request.body !== null) {
972
+ if (request.body != null) {
957
973
  assert(request.body.source)
958
974
  request.body = safelyExtractBody(request.body.source)[0]
959
975
  }
@@ -1058,7 +1074,7 @@ async function httpNetworkOrCacheFetch (
1058
1074
 
1059
1075
  // 7. If contentLength is non-null, then set contentLengthHeaderValue to
1060
1076
  // contentLength, serialized and isomorphic encoded.
1061
- if (contentLength !== null) {
1077
+ if (contentLength != null) {
1062
1078
  // TODO: isomorphic encoded
1063
1079
  contentLengthHeaderValue = String(contentLength)
1064
1080
  }
@@ -1066,13 +1082,13 @@ async function httpNetworkOrCacheFetch (
1066
1082
  // 8. If contentLengthHeaderValue is non-null, then append
1067
1083
  // `Content-Length`/contentLengthHeaderValue to httpRequest’s header
1068
1084
  // list.
1069
- if (contentLengthHeaderValue !== null) {
1085
+ if (contentLengthHeaderValue != null) {
1070
1086
  httpRequest.headersList.append('content-length', contentLengthHeaderValue)
1071
1087
  }
1072
1088
 
1073
1089
  // 9. If contentLength is non-null and httpRequest’s keepalive is true,
1074
1090
  // then:
1075
- if (contentLength !== null && httpRequest.keepalive) {
1091
+ if (contentLength != null && httpRequest.keepalive) {
1076
1092
  // NOTE: keepalive is a noop outside of browser context.
1077
1093
  }
1078
1094
 
@@ -1259,11 +1275,15 @@ async function httpNetworkOrCacheFetch (
1259
1275
  // 3. If the ongoing fetch is terminated, then:
1260
1276
  if (context.terminated) {
1261
1277
  // 1. Let aborted be the termination’s aborted flag.
1278
+ const aborted = context.terminated.aborted
1279
+
1262
1280
  // 2. If aborted is set, then return an aborted network error.
1281
+ if (aborted) {
1282
+ return makeNetworkError(new AbortError())
1283
+ }
1284
+
1263
1285
  // 3. Return a network error.
1264
- return makeNetworkError(
1265
- context.terminated.aborted ? new AbortError() : null
1266
- )
1286
+ return makeNetworkError(context.terminated.reason)
1267
1287
  }
1268
1288
 
1269
1289
  // 4. Prompt the end user as appropriate in request’s window and store
@@ -1283,7 +1303,7 @@ async function httpNetworkOrCacheFetch (
1283
1303
  // isNewConnectionFetch is false
1284
1304
  !isNewConnectionFetch &&
1285
1305
  // request’s body is null, or request’s body is non-null and request’s body’s source is non-null
1286
- (request.body == null || request.body.source !== null)
1306
+ (request.body == null || request.body.source != null)
1287
1307
  ) {
1288
1308
  // then:
1289
1309
 
@@ -1293,10 +1313,12 @@ async function httpNetworkOrCacheFetch (
1293
1313
  const aborted = context.terminated.aborted
1294
1314
 
1295
1315
  // 2. If aborted is set, then return an aborted network error.
1296
- const reason = aborted ? new AbortError() : new Error('terminated')
1316
+ if (aborted) {
1317
+ return makeNetworkError(new AbortError())
1318
+ }
1297
1319
 
1298
1320
  // 3. Return a network error.
1299
- return makeNetworkError(reason)
1321
+ return makeNetworkError(context.terminated.reason)
1300
1322
  }
1301
1323
 
1302
1324
  // 2. Set response to the result of running HTTP-network-or-cache
@@ -1335,41 +1357,16 @@ function httpNetworkFetch (
1335
1357
  return new Promise((resolve) => {
1336
1358
  assert(!context.connection || context.connection.destroyed)
1337
1359
 
1338
- const connection = (context.connection = {
1360
+ context.connection = {
1339
1361
  abort: null,
1340
- controller: null,
1341
1362
  destroyed: false,
1342
- errored: false,
1343
- dump: false,
1344
1363
  destroy (err) {
1345
- if (this.destroyed) {
1346
- return
1347
- }
1348
-
1349
- this.destroyed = true
1350
-
1351
- if (this.abort) {
1352
- this.abort()
1353
- this.abort = null
1354
- }
1355
-
1356
- if (err) {
1357
- this.errored = err
1358
- }
1359
-
1360
- if (this.controller) {
1361
- try {
1362
- this.controller.error(err ?? new AbortError())
1363
- this.controller = null
1364
- } catch (err) {
1365
- // Will throw TypeError if body is not readable.
1366
- if (err.name !== 'TypeError') {
1367
- throw err
1368
- }
1369
- }
1364
+ if (!this.destroyed) {
1365
+ this.destroyed = true
1366
+ this.abort?.(err ?? new AbortError())
1370
1367
  }
1371
1368
  }
1372
- })
1369
+ }
1373
1370
 
1374
1371
  // 1. Let request be fetchParams’s request.
1375
1372
  const request = fetchParams.request
@@ -1505,16 +1502,18 @@ function httpNetworkFetch (
1505
1502
  // 9. If aborted, then:
1506
1503
  function onRequestAborted () {
1507
1504
  // 1. Let aborted be the termination’s aborted flag.
1508
- const aborted = context.terminated.aborted
1505
+ const aborted = this.terminated.aborted
1509
1506
 
1510
1507
  // 2. If connection uses HTTP/2, then transmit an RST_STREAM frame.
1511
- connection.destroy()
1508
+ this.connection.destroy()
1512
1509
 
1513
1510
  // 3. If aborted is set, then return an aborted network error.
1514
- const reason = aborted ? new AbortError() : new Error('terminated')
1511
+ if (aborted) {
1512
+ return resolve(makeNetworkError(new AbortError()))
1513
+ }
1515
1514
 
1516
1515
  // 4. Return a network error.
1517
- resolve(makeNetworkError(reason))
1516
+ return resolve(makeNetworkError(this.terminated.reason))
1518
1517
  }
1519
1518
 
1520
1519
  // 10. Let pullAlgorithm be an action that resumes the ongoing fetch
@@ -1529,7 +1528,7 @@ function httpNetworkFetch (
1529
1528
 
1530
1529
  // 12. Let highWaterMark be a non-negative, non-NaN number, chosen by
1531
1530
  // the user agent.
1532
- const highWaterMark = 65536
1531
+ const highWaterMark = 64 * 1024 // Same as nodejs fs streams.
1533
1532
 
1534
1533
  // 13. Let sizeAlgorithm be an algorithm that accepts a chunk object
1535
1534
  // and returns a non-negative, non-NaN, non-infinite number, chosen by the user agent.
@@ -1543,20 +1542,23 @@ function httpNetworkFetch (
1543
1542
  ReadableStream = require('stream/web').ReadableStream
1544
1543
  }
1545
1544
 
1545
+ let pullResolve
1546
+
1546
1547
  const stream = new ReadableStream(
1547
1548
  {
1548
1549
  async start (controller) {
1549
- connection.controller = controller
1550
+ context.controller = controller
1550
1551
  },
1551
- async pull () {
1552
- if (pullAlgorithm) {
1553
- pullAlgorithm()
1554
- } else {
1555
- pullAlgorithm = null
1552
+ async pull (controller) {
1553
+ if (!pullAlgorithm) {
1554
+ await new Promise((resolve) => {
1555
+ pullResolve = resolve
1556
+ })
1556
1557
  }
1558
+ await pullAlgorithm(controller)
1557
1559
  },
1558
1560
  async cancel (reason) {
1559
- cancelAlgorithm()
1561
+ await cancelAlgorithm(reason)
1560
1562
  }
1561
1563
  },
1562
1564
  { highWaterMark }
@@ -1586,7 +1588,7 @@ function httpNetworkFetch (
1586
1588
  finalizeResponse(fetchParams, response)
1587
1589
 
1588
1590
  // 2. Let aborted be the termination’s aborted flag.
1589
- const aborted = context.terminated.aborted
1591
+ const aborted = this.terminated.aborted
1590
1592
 
1591
1593
  // 3. If aborted is set, then:
1592
1594
  if (aborted) {
@@ -1594,15 +1596,19 @@ function httpNetworkFetch (
1594
1596
  response.aborted = true
1595
1597
 
1596
1598
  // 2. If stream is readable, error stream with an "AbortError" DOMException.
1597
- connection.destroy(new AbortError())
1599
+ if (isReadable(stream)) {
1600
+ this.controller.error(new AbortError())
1601
+ }
1598
1602
  } else {
1599
1603
  // 4. Otherwise, if stream is readable, error stream with a TypeError.
1600
- connection.destroy(new TypeError('terminated'))
1604
+ if (isReadable(stream)) {
1605
+ this.controller.error(new TypeError('terminated'))
1606
+ }
1601
1607
  }
1602
1608
 
1603
1609
  // 5. If connection uses HTTP/2, then transmit an RST_STREAM frame.
1604
1610
  // 6. Otherwise, the user agent should close connection unless it would be bad for performance to do so.
1605
- connection.destroy()
1611
+ this.connection.destroy()
1606
1612
  }
1607
1613
 
1608
1614
  // 19. Return response.
@@ -1621,12 +1627,17 @@ function httpNetworkFetch (
1621
1627
  },
1622
1628
  {
1623
1629
  decoder: null,
1630
+ abort: null,
1631
+ context,
1624
1632
 
1625
1633
  onConnect (abort) {
1634
+ // TODO (fix): Do we need connection here?
1635
+ const { connection } = this.context
1636
+
1626
1637
  if (connection.destroyed) {
1627
1638
  abort(new AbortError())
1628
1639
  } else {
1629
- connection.abort = abort
1640
+ this.abort = connection.abort = abort
1630
1641
  }
1631
1642
  },
1632
1643
 
@@ -1643,19 +1654,14 @@ function httpNetworkFetch (
1643
1654
  )
1644
1655
  }
1645
1656
 
1646
- const hasPulled = pullAlgorithm !== undefined
1647
-
1648
- const body = { stream }
1649
- registry.register(body, connection.abort)
1650
-
1651
1657
  response = makeResponse({
1652
1658
  status,
1653
1659
  statusText,
1654
1660
  headersList: headers[kHeadersList],
1655
- body
1661
+ body: { stream }
1656
1662
  })
1657
1663
 
1658
- context.on('terminated', onResponseAborted)
1664
+ this.context.on('terminated', onResponseAborted)
1659
1665
 
1660
1666
  const codings =
1661
1667
  headers
@@ -1680,31 +1686,18 @@ function httpNetworkFetch (
1680
1686
  }
1681
1687
  }
1682
1688
 
1683
- let iterator
1684
-
1685
1689
  if (decoders.length > 1) {
1686
- if (compose) {
1687
- this.decoder = compose(...decoders)
1688
- iterator = this.decoder[Symbol.asyncIterator]()
1689
- } else {
1690
- this.decoder = new PassThrough()
1691
- iterator = pipeline(this.decoder, ...decoders, () => {})[
1692
- Symbol.asyncIterator
1693
- ]()
1694
- }
1695
- } else if (decoders.length === 1) {
1696
- this.decoder = decoders[0]
1697
- iterator = this.decoder[Symbol.asyncIterator]()
1698
- } else {
1699
- this.decoder = new PassThrough()
1700
- iterator = this.decoder[Symbol.asyncIterator]()
1690
+ pipeline(...decoders, () => {})
1691
+ } else if (decoders.length === 0) {
1692
+ // TODO (perf): Avoid intermediate.
1693
+ decoders.push(new PassThrough())
1701
1694
  }
1702
1695
 
1703
- if (this.decoder) {
1704
- this.decoder.on('drain', resume)
1705
- }
1696
+ this.decoder = decoders[0].on('drain', resume)
1697
+
1698
+ const iterator = decoders[decoders.length - 1][Symbol.asyncIterator]()
1706
1699
 
1707
- pullAlgorithm = async () => {
1700
+ pullAlgorithm = async (controller) => {
1708
1701
  // 4. Set bytes to the result of handling content codings given
1709
1702
  // codings and bytes.
1710
1703
  let bytes
@@ -1720,10 +1713,6 @@ function httpNetworkFetch (
1720
1713
  }
1721
1714
  }
1722
1715
 
1723
- if (!connection.controller) {
1724
- return
1725
- }
1726
-
1727
1716
  if (bytes === undefined) {
1728
1717
  // 2. Otherwise, if the bytes transmission for response’s message
1729
1718
  // body is done normally and stream is readable, then close
@@ -1731,13 +1720,7 @@ function httpNetworkFetch (
1731
1720
  // abort these in-parallel steps.
1732
1721
  finalizeResponse(fetchParams, response)
1733
1722
 
1734
- context.off('terminated', onResponseAborted)
1735
- context.off('terminated', onRequestAborted)
1736
-
1737
- connection.controller.close()
1738
- connection.controller = null
1739
-
1740
- connection.destroy()
1723
+ controller.close()
1741
1724
 
1742
1725
  return
1743
1726
  }
@@ -1747,27 +1730,28 @@ function httpNetworkFetch (
1747
1730
 
1748
1731
  // 6. If bytes is failure, then terminate the ongoing fetch.
1749
1732
  if (bytes instanceof Error) {
1750
- context.terminate({ reason: bytes })
1733
+ this.context.terminate({ reason: bytes })
1751
1734
  return
1752
1735
  }
1753
1736
 
1754
1737
  // 7. Enqueue a Uint8Array wrapping an ArrayBuffer containing bytes
1755
- // into stream.
1756
- connection.controller.enqueue(new Uint8Array(bytes))
1738
+ // into stream.
1739
+ controller.enqueue(new Uint8Array(bytes))
1757
1740
 
1758
1741
  // 8. If stream is errored, then terminate the ongoing fetch.
1759
- if (connection.errored) {
1760
- context.terminate({ reason: connection.errored })
1742
+ if (isErrored(stream)) {
1743
+ this.context.terminate()
1761
1744
  return
1762
1745
  }
1763
1746
 
1764
1747
  // 9. If stream doesn’t need more data ask the user agent to suspend
1765
- // the ongoing fetch.
1766
- return connection.controller.desiredSize > 0
1748
+ // the ongoing fetch.
1749
+ return controller.desiredSize > 0
1767
1750
  }
1768
1751
 
1769
- if (hasPulled) {
1770
- pullAlgorithm()
1752
+ if (pullResolve) {
1753
+ pullResolve()
1754
+ pullResolve = null
1771
1755
  }
1772
1756
 
1773
1757
  resolve(response)
@@ -1776,7 +1760,7 @@ function httpNetworkFetch (
1776
1760
  },
1777
1761
 
1778
1762
  onData (chunk) {
1779
- if (connection.dump) {
1763
+ if (this.context.dump) {
1780
1764
  return
1781
1765
  }
1782
1766
 
@@ -1798,17 +1782,14 @@ function httpNetworkFetch (
1798
1782
  return this.decoder.write(bytes)
1799
1783
  },
1800
1784
 
1801
- async onComplete () {
1785
+ onComplete () {
1802
1786
  this.decoder.end()
1803
1787
  },
1804
1788
 
1805
1789
  onError (error) {
1806
- context.off('terminated', onResponseAborted)
1807
- context.off('terminated', onRequestAborted)
1808
-
1809
- connection.destroy(error)
1790
+ this.decoder?.destroy(error)
1810
1791
 
1811
- context.terminate({ reason: error })
1792
+ this.context.terminate({ reason: error })
1812
1793
 
1813
1794
  if (!response) {
1814
1795
  resolve(makeNetworkError(error))
@@ -28,6 +28,10 @@ let TransformStream
28
28
 
29
29
  const kInit = Symbol('init')
30
30
 
31
+ const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
32
+ signal.removeEventListener('abort', abort)
33
+ })
34
+
31
35
  // https://fetch.spec.whatwg.org/#request-class
32
36
  class Request {
33
37
  // https://fetch.spec.whatwg.org/#dom-request
@@ -123,7 +127,7 @@ class Request {
123
127
  }
124
128
 
125
129
  // 10. If init["window"] exists and is non-null, then throw a TypeError.
126
- if ('window' in init && window !== null) {
130
+ if ('window' in init && window != null) {
127
131
  throw new TypeError(`'window' option '${window}' must be null`)
128
132
  }
129
133
 
@@ -134,8 +138,48 @@ class Request {
134
138
 
135
139
  // 12. Set request to a new request with the following properties:
136
140
  request = makeRequest({
137
- ...request,
138
- window
141
+ // URL request’s URL.
142
+ // undici implementation note: this is set as the first item in request's urlList in makeRequest
143
+ // method request’s method.
144
+ method: request.method,
145
+ // header list A copy of request’s header list.
146
+ // undici implementation note: headersList is cloned in makeRequest
147
+ headersList: request.headersList,
148
+ // unsafe-request flag Set.
149
+ unsafeRequest: request.unsafeRequest,
150
+ // client This’s relevant settings object.
151
+ client: request.client,
152
+ // window window.
153
+ window,
154
+ // priority request’s priority.
155
+ priority: request.priority,
156
+ // origin request’s origin. The propagation of the origin is only significant for navigation requests
157
+ // being handled by a service worker. In this scenario a request can have an origin that is different
158
+ // from the current client.
159
+ origin: request.origin,
160
+ // referrer request’s referrer.
161
+ referrer: request.referrer,
162
+ // referrer policy request’s referrer policy.
163
+ referrerPolicy: request.referrerPolicy,
164
+ // mode request’s mode.
165
+ mode: request.mode,
166
+ // credentials mode request’s credentials mode.
167
+ credentials: request.credentials,
168
+ // cache mode request’s cache mode.
169
+ cache: request.cache,
170
+ // redirect mode request’s redirect mode.
171
+ redirect: request.redirect,
172
+ // integrity metadata request’s integrity metadata.
173
+ integrity: request.integrity,
174
+ // keepalive request’s keepalive.
175
+ keepalive: request.keepalive,
176
+ // reload-navigation flag request’s reload-navigation flag.
177
+ reloadNavigation: request.reloadNavigation,
178
+ // history-navigation flag request’s history-navigation flag.
179
+ historyNavigation: request.historyNavigation,
180
+ // URL list A clone of request’s URL list.
181
+ // undici implementation note: urlList is cloned in makeRequest
182
+ urlList: request.urlList
139
183
  })
140
184
 
141
185
  // 13. If init is not empty, then:
@@ -151,11 +195,20 @@ class Request {
151
195
  // 3. Unset request’s history-navigation flag.
152
196
  request.historyNavigation = false
153
197
 
154
- // 4. Set request’s referrer to "client"
198
+ // 4. Set request’s origin to "client".
199
+ request.origin = 'client'
200
+
201
+ // 5. Set request’s referrer to "client"
155
202
  request.referrer = 'client'
156
203
 
157
- // 5. Set request’s referrer policy to the empty string.
204
+ // 6. Set request’s referrer policy to the empty string.
158
205
  request.referrerPolicy = ''
206
+
207
+ // 7. Set request’s URL to request’s current URL.
208
+ request.url = request.urlList[request.urlList.length - 1]
209
+
210
+ // 8. Set request’s URL list to « request’s URL ».
211
+ request.urlList = [request.url]
159
212
  }
160
213
 
161
214
  // 14. If init["referrer"] exists, then:
@@ -164,7 +217,7 @@ class Request {
164
217
  const referrer = init.referrer
165
218
 
166
219
  // 2. If referrer is the empty string, then set request’s referrer to "no-referrer".
167
- if (!referrer === '') {
220
+ if (referrer === '') {
168
221
  request.referrer = 'no-referrer'
169
222
  } else {
170
223
  // 1. Let parsedReferrer be the result of parsing referrer with
@@ -223,7 +276,7 @@ class Request {
223
276
  }
224
277
 
225
278
  // 18. If mode is non-null, set request’s mode to mode.
226
- if (mode !== null) {
279
+ if (mode != null) {
227
280
  request.mode = mode
228
281
  }
229
282
 
@@ -328,14 +381,9 @@ class Request {
328
381
  if (signal.aborted) {
329
382
  ac.abort()
330
383
  } else {
331
- // TODO: Remove this listener on failure/success.
332
- signal.addEventListener(
333
- 'abort',
334
- function () {
335
- ac.abort()
336
- },
337
- { once: true }
338
- )
384
+ const abort = () => ac.abort()
385
+ signal.addEventListener('abort', abort, { once: true })
386
+ requestFinalizer.register(this, { signal, abort })
339
387
  }
340
388
  }
341
389
 
@@ -769,7 +817,7 @@ function cloneRequest (request) {
769
817
 
770
818
  // 2. If request’s body is non-null, set newRequest’s body to the
771
819
  // result of cloning request’s body.
772
- if (request.body !== null) {
820
+ if (request.body != null) {
773
821
  newRequest.body = cloneBody(request.body)
774
822
  }
775
823
 
@@ -327,7 +327,7 @@ function cloneResponse (response) {
327
327
 
328
328
  // 3. If response’s body is non-null, then set newResponse’s body to the
329
329
  // result of cloning response’s body.
330
- if (response.body !== null) {
330
+ if (response.body != null) {
331
331
  newResponse.body = cloneBody(response.body)
332
332
  }
333
333
 
package/lib/fetch/util.js CHANGED
@@ -253,20 +253,20 @@ function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) {
253
253
  return performance.now()
254
254
  }
255
255
 
256
- function makeTimingInfo (init) {
256
+ // https://fetch.spec.whatwg.org/#create-an-opaque-timing-info
257
+ function createOpaqueTimingInfo (timingInfo) {
257
258
  return {
258
- startTime: 0,
259
+ startTime: timingInfo.startTime ?? 0,
259
260
  redirectStartTime: 0,
260
261
  redirectEndTime: 0,
261
- postRedirectStartTime: 0,
262
+ postRedirectStartTime: timingInfo.startTime ?? 0,
262
263
  finalServiceWorkerStartTime: 0,
263
264
  finalNetworkResponseStartTime: 0,
264
265
  finalNetworkRequestStartTime: 0,
265
266
  endTime: 0,
266
267
  encodedBodySize: 0,
267
268
  decodedBodySize: 0,
268
- finalConnectionTimingInfo: null,
269
- ...init
269
+ finalConnectionTimingInfo: null
270
270
  }
271
271
  }
272
272
 
@@ -318,8 +318,7 @@ module.exports = {
318
318
  TAOCheck,
319
319
  corsCheck,
320
320
  crossOriginResourcePolicyCheck,
321
- ReadableStreamFrom,
322
- makeTimingInfo,
321
+ createOpaqueTimingInfo,
323
322
  setRequestReferrerPolicyOnRedirect,
324
323
  isValidHTTPToken,
325
324
  requestBadPort,
package/lib/pool-base.js CHANGED
@@ -7,7 +7,7 @@ const {
7
7
  InvalidArgumentError
8
8
  } = require('./core/errors')
9
9
  const FixedQueue = require('./node/fixed-queue')
10
- const { kSize, kRunning, kPending, kBusy } = require('./core/symbols')
10
+ const { kSize, kRunning, kPending, kBusy, kUrl } = require('./core/symbols')
11
11
 
12
12
  const kClients = Symbol('clients')
13
13
  const kNeedDrain = Symbol('needDrain')
@@ -257,5 +257,5 @@ module.exports = {
257
257
  kNeedDrain,
258
258
  kAddClient,
259
259
  kRemoveClient,
260
- kDispatch,
260
+ kDispatch
261
261
  }
package/lib/pool.js CHANGED
@@ -5,11 +5,11 @@ const {
5
5
  kClients,
6
6
  kNeedDrain,
7
7
  kAddClient,
8
- kDispatch,
9
- } = require('./pool-base')
8
+ kDispatch
9
+ } = require('./pool-base')
10
10
  const Client = require('./client')
11
11
  const {
12
- InvalidArgumentError,
12
+ InvalidArgumentError
13
13
  } = require('./core/errors')
14
14
  const util = require('./core/util')
15
15
  const { kUrl } = require('./core/symbols')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "4.11.0",
3
+ "version": "4.12.0",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {