undici 7.2.0 → 7.2.2

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.
@@ -40,7 +40,7 @@ diagnosticsChannel.channel('undici:request:bodySent').subscribe(({ request }) =>
40
40
 
41
41
  ## `undici:request:headers`
42
42
 
43
- This message is published after the response headers have been received, i.e. the response has been completed.
43
+ This message is published after the response headers have been received.
44
44
 
45
45
  ```js
46
46
  import diagnosticsChannel from 'diagnostics_channel'
@@ -652,7 +652,7 @@ return null
652
652
 
653
653
  A faster version of `Dispatcher.request`. This method expects the second argument `factory` to return a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream which the response will be written to. This improves performance by avoiding creating an intermediate [`stream.Readable`](https://nodejs.org/api/stream.html#stream_readable_streams) stream when the user expects to directly pipe the response body to a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream.
654
654
 
655
- As demonstrated in [Example 1 - Basic GET stream request](/docs/docs/api/Dispatcher.md#example-1---basic-get-stream-request), it is recommended to use the `option.opaque` property to avoid creating a closure for the `factory` method. This pattern works well with Node.js Web Frameworks such as [Fastify](https://fastify.io). See [Example 2 - Stream to Fastify Response](/docs/docs/api/Dispatch.md#example-2---stream-to-fastify-response) for more details.
655
+ As demonstrated in [Example 1 - Basic GET stream request](/docs/docs/api/Dispatcher.md#example-1-basic-get-stream-request), it is recommended to use the `option.opaque` property to avoid creating a closure for the `factory` method. This pattern works well with Node.js Web Frameworks such as [Fastify](https://fastify.io). See [Example 2 - Stream to Fastify Response](/docs/docs/api/Dispatch.md#example-2-stream-to-fastify-response) for more details.
656
656
 
657
657
  Arguments:
658
658
 
package/lib/core/util.js CHANGED
@@ -600,20 +600,25 @@ function ReadableStreamFrom (iterable) {
600
600
  async start () {
601
601
  iterator = iterable[Symbol.asyncIterator]()
602
602
  },
603
- async pull (controller) {
604
- const { done, value } = await iterator.next()
605
- if (done) {
606
- queueMicrotask(() => {
607
- controller.close()
608
- controller.byobRequest?.respond(0)
609
- })
610
- } else {
611
- const buf = Buffer.isBuffer(value) ? value : Buffer.from(value)
612
- if (buf.byteLength) {
613
- controller.enqueue(new Uint8Array(buf))
603
+ pull (controller) {
604
+ async function pull () {
605
+ const { done, value } = await iterator.next()
606
+ if (done) {
607
+ queueMicrotask(() => {
608
+ controller.close()
609
+ controller.byobRequest?.respond(0)
610
+ })
611
+ } else {
612
+ const buf = Buffer.isBuffer(value) ? value : Buffer.from(value)
613
+ if (buf.byteLength) {
614
+ controller.enqueue(new Uint8Array(buf))
615
+ } else {
616
+ return await pull()
617
+ }
614
618
  }
615
619
  }
616
- return controller.desiredSize > 0
620
+
621
+ return pull()
617
622
  },
618
623
  async cancel () {
619
624
  await iterator.return()
@@ -133,7 +133,7 @@ class RetryHandler {
133
133
  ? Math.min(retryAfterHeader, maxTimeout)
134
134
  : Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout)
135
135
 
136
- setTimeout(() => cb(null), retryTimeout).unref()
136
+ setTimeout(() => cb(null), retryTimeout)
137
137
  }
138
138
 
139
139
  onResponseStart (controller, statusCode, headers, statusMessage) {
@@ -277,7 +277,7 @@ class RetryHandler {
277
277
  }
278
278
 
279
279
  onResponseError (controller, err) {
280
- if (!controller || controller.aborted || isDisturbed(this.opts.body)) {
280
+ if (controller?.aborted || isDisturbed(this.opts.body)) {
281
281
  this.handler.onResponseError?.(controller, err)
282
282
  return
283
283
  }
@@ -32,7 +32,7 @@ class DNSInstance {
32
32
 
33
33
  // If full, we just return the origin
34
34
  if (ips == null && this.full) {
35
- cb(null, origin.origin)
35
+ cb(null, origin)
36
36
  return
37
37
  }
38
38
 
@@ -74,9 +74,9 @@ class DNSInstance {
74
74
 
75
75
  cb(
76
76
  null,
77
- `${origin.protocol}//${
77
+ new URL(`${origin.protocol}//${
78
78
  ip.family === 6 ? `[${ip.address}]` : ip.address
79
- }${port}`
79
+ }${port}`)
80
80
  )
81
81
  })
82
82
  } else {
@@ -105,9 +105,9 @@ class DNSInstance {
105
105
 
106
106
  cb(
107
107
  null,
108
- `${origin.protocol}//${
108
+ new URL(`${origin.protocol}//${
109
109
  ip.family === 6 ? `[${ip.address}]` : ip.address
110
- }${port}`
110
+ }${port}`)
111
111
  )
112
112
  }
113
113
  }
@@ -192,6 +192,38 @@ class DNSInstance {
192
192
  return ip
193
193
  }
194
194
 
195
+ pickFamily (origin, ipFamily) {
196
+ const records = this.#records.get(origin.hostname)?.records
197
+ if (!records) {
198
+ return null
199
+ }
200
+
201
+ const family = records[ipFamily]
202
+ if (!family) {
203
+ return null
204
+ }
205
+
206
+ if (family.offset == null || family.offset === maxInt) {
207
+ family.offset = 0
208
+ } else {
209
+ family.offset++
210
+ }
211
+
212
+ const position = family.offset % family.ips.length
213
+ const ip = family.ips[position] ?? null
214
+ if (ip == null) {
215
+ return ip
216
+ }
217
+
218
+ if (Date.now() - ip.timestamp > ip.ttl) { // record TTL is already in ms
219
+ // We delete expired records
220
+ // It is possible that they have different TTL, so we manage them individually
221
+ family.ips.splice(position, 1)
222
+ }
223
+
224
+ return ip
225
+ }
226
+
195
227
  setRecords (origin, addresses) {
196
228
  const timestamp = Date.now()
197
229
  const records = { records: { 4: null, 6: null } }
@@ -228,10 +260,13 @@ class DNSDispatchHandler extends DecoratorHandler {
228
260
  #dispatch = null
229
261
  #origin = null
230
262
  #controller = null
263
+ #newOrigin = null
264
+ #firstTry = true
231
265
 
232
- constructor (state, { origin, handler, dispatch }, opts) {
266
+ constructor (state, { origin, handler, dispatch, newOrigin }, opts) {
233
267
  super(handler)
234
268
  this.#origin = origin
269
+ this.#newOrigin = newOrigin
235
270
  this.#opts = { ...opts }
236
271
  this.#state = state
237
272
  this.#dispatch = dispatch
@@ -242,21 +277,36 @@ class DNSDispatchHandler extends DecoratorHandler {
242
277
  case 'ETIMEDOUT':
243
278
  case 'ECONNREFUSED': {
244
279
  if (this.#state.dualStack) {
245
- // We delete the record and retry
246
- this.#state.runLookup(this.#origin, this.#opts, (err, newOrigin) => {
247
- if (err) {
248
- super.onResponseError(controller, err)
249
- return
250
- }
251
-
252
- const dispatchOpts = {
253
- ...this.#opts,
254
- origin: newOrigin
255
- }
280
+ if (!this.#firstTry) {
281
+ super.onResponseError(controller, err)
282
+ return
283
+ }
284
+ this.#firstTry = false
285
+
286
+ // Pick an ip address from the other family
287
+ const otherFamily = this.#newOrigin.hostname[0] === '[' ? 4 : 6
288
+ const ip = this.#state.pickFamily(this.#origin, otherFamily)
289
+ if (ip == null) {
290
+ super.onResponseError(controller, err)
291
+ return
292
+ }
256
293
 
257
- this.#dispatch(dispatchOpts, this)
258
- })
294
+ let port
295
+ if (typeof ip.port === 'number') {
296
+ port = `:${ip.port}`
297
+ } else if (this.#origin.port !== '') {
298
+ port = `:${this.#origin.port}`
299
+ } else {
300
+ port = ''
301
+ }
259
302
 
303
+ const dispatchOpts = {
304
+ ...this.#opts,
305
+ origin: `${this.#origin.protocol}//${
306
+ ip.family === 6 ? `[${ip.address}]` : ip.address
307
+ }${port}`
308
+ }
309
+ this.#dispatch(dispatchOpts, this)
260
310
  return
261
311
  }
262
312
 
@@ -266,7 +316,8 @@ class DNSDispatchHandler extends DecoratorHandler {
266
316
  }
267
317
  case 'ENOTFOUND':
268
318
  this.#state.deleteRecords(this.#origin)
269
- // eslint-disable-next-line no-fallthrough
319
+ super.onResponseError(controller, err)
320
+ break
270
321
  default:
271
322
  super.onResponseError(controller, err)
272
323
  break
@@ -353,14 +404,13 @@ module.exports = interceptorOpts => {
353
404
 
354
405
  instance.runLookup(origin, origDispatchOpts, (err, newOrigin) => {
355
406
  if (err) {
356
- return handler.onError(err)
407
+ return handler.onResponseError(null, err)
357
408
  }
358
409
 
359
- let dispatchOpts = null
360
- dispatchOpts = {
410
+ const dispatchOpts = {
361
411
  ...origDispatchOpts,
362
412
  servername: origin.hostname, // For SNI on TLS
363
- origin: newOrigin,
413
+ origin: newOrigin.origin,
364
414
  headers: {
365
415
  host: origin.host,
366
416
  ...origDispatchOpts.headers
@@ -369,7 +419,10 @@ module.exports = interceptorOpts => {
369
419
 
370
420
  dispatch(
371
421
  dispatchOpts,
372
- instance.getHandler({ origin, dispatch, handler }, origDispatchOpts)
422
+ instance.getHandler(
423
+ { origin, dispatch, handler, newOrigin },
424
+ origDispatchOpts
425
+ )
373
426
  )
374
427
  })
375
428
 
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require('./constants')
4
- const { failWebsocketConnection, parseExtensions, isClosed, isClosing, isEstablished, validateCloseCodeAndReason } = require('./util')
4
+ const { parseExtensions, isClosed, isClosing, isEstablished, validateCloseCodeAndReason } = require('./util')
5
5
  const { channels } = require('../../core/diagnostics')
6
6
  const { makeRequest } = require('../fetch/request')
7
7
  const { fetching } = require('../fetch/index')
@@ -294,7 +294,32 @@ function closeWebSocketConnection (object, code, reason, validate = false) {
294
294
  }
295
295
  }
296
296
 
297
+ /**
298
+ * @param {import('./websocket').Handler} handler
299
+ * @param {number} code
300
+ * @param {string|undefined} reason
301
+ * @returns {void}
302
+ */
303
+ function failWebsocketConnection (handler, code, reason) {
304
+ // If _The WebSocket Connection is Established_ prior to the point where
305
+ // the endpoint is required to _Fail the WebSocket Connection_, the
306
+ // endpoint SHOULD send a Close frame with an appropriate status code
307
+ // (Section 7.4) before proceeding to _Close the WebSocket Connection_.
308
+ if (isEstablished(handler.readyState)) {
309
+ closeWebSocketConnection(handler, code, reason, false)
310
+ }
311
+
312
+ handler.controller.abort()
313
+
314
+ if (handler.socket?.destroyed === false) {
315
+ handler.socket.destroy()
316
+ }
317
+
318
+ handler.onFail(code, reason)
319
+ }
320
+
297
321
  module.exports = {
298
322
  establishWebSocketConnection,
323
+ failWebsocketConnection,
299
324
  closeWebSocketConnection
300
325
  }
@@ -7,13 +7,13 @@ const { channels } = require('../../core/diagnostics')
7
7
  const {
8
8
  isValidStatusCode,
9
9
  isValidOpcode,
10
- failWebsocketConnection,
11
10
  websocketMessageReceived,
12
11
  utf8Decode,
13
12
  isControlFrame,
14
13
  isTextBinaryFrame,
15
14
  isContinuationFrame
16
15
  } = require('./util')
16
+ const { failWebsocketConnection } = require('./connection')
17
17
  const { WebsocketFrameSend } = require('./frame')
18
18
  const { PerMessageDeflate } = require('./permessage-deflate')
19
19
 
@@ -24,6 +24,7 @@ const { PerMessageDeflate } = require('./permessage-deflate')
24
24
 
25
25
  class ByteParser extends Writable {
26
26
  #buffers = []
27
+ #fragmentsBytes = 0
27
28
  #byteOffset = 0
28
29
  #loop = false
29
30
 
@@ -208,16 +209,14 @@ class ByteParser extends Writable {
208
209
  this.#state = parserStates.INFO
209
210
  } else {
210
211
  if (!this.#info.compressed) {
211
- this.#fragments.push(body)
212
+ this.writeFragments(body)
212
213
 
213
214
  // If the frame is not fragmented, a message has been received.
214
215
  // If the frame is fragmented, it will terminate with a fin bit set
215
216
  // and an opcode of 0 (continuation), therefore we handle that when
216
217
  // parsing continuation frames, not here.
217
218
  if (!this.#info.fragmented && this.#info.fin) {
218
- const fullMessage = Buffer.concat(this.#fragments)
219
- websocketMessageReceived(this.#handler, this.#info.binaryType, fullMessage)
220
- this.#fragments.length = 0
219
+ websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments())
221
220
  }
222
221
 
223
222
  this.#state = parserStates.INFO
@@ -228,7 +227,7 @@ class ByteParser extends Writable {
228
227
  return
229
228
  }
230
229
 
231
- this.#fragments.push(data)
230
+ this.writeFragments(data)
232
231
 
233
232
  if (!this.#info.fin) {
234
233
  this.#state = parserStates.INFO
@@ -237,11 +236,10 @@ class ByteParser extends Writable {
237
236
  return
238
237
  }
239
238
 
240
- websocketMessageReceived(this.#handler, this.#info.binaryType, Buffer.concat(this.#fragments))
239
+ websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments())
241
240
 
242
241
  this.#loop = true
243
242
  this.#state = parserStates.INFO
244
- this.#fragments.length = 0
245
243
  this.run(callback)
246
244
  })
247
245
 
@@ -265,34 +263,70 @@ class ByteParser extends Writable {
265
263
  return emptyBuffer
266
264
  }
267
265
 
268
- if (this.#buffers[0].length === n) {
269
- this.#byteOffset -= this.#buffers[0].length
266
+ this.#byteOffset -= n
267
+
268
+ const first = this.#buffers[0]
269
+
270
+ if (first.length > n) {
271
+ // replace with remaining buffer
272
+ this.#buffers[0] = first.subarray(n, first.length)
273
+ return first.subarray(0, n)
274
+ } else if (first.length === n) {
275
+ // prefect match
270
276
  return this.#buffers.shift()
277
+ } else {
278
+ let offset = 0
279
+ // If Buffer.allocUnsafe is used, extra copies will be made because the offset is non-zero.
280
+ const buffer = Buffer.allocUnsafeSlow(n)
281
+ while (offset !== n) {
282
+ const next = this.#buffers[0]
283
+ const length = next.length
284
+
285
+ if (length + offset === n) {
286
+ buffer.set(this.#buffers.shift(), offset)
287
+ break
288
+ } else if (length + offset > n) {
289
+ buffer.set(next.subarray(0, n - offset), offset)
290
+ this.#buffers[0] = next.subarray(n - offset)
291
+ break
292
+ } else {
293
+ buffer.set(this.#buffers.shift(), offset)
294
+ offset += length
295
+ }
296
+ }
297
+
298
+ return buffer
299
+ }
300
+ }
301
+
302
+ writeFragments (fragment) {
303
+ this.#fragmentsBytes += fragment.length
304
+ this.#fragments.push(fragment)
305
+ }
306
+
307
+ consumeFragments () {
308
+ const fragments = this.#fragments
309
+
310
+ if (fragments.length === 1) {
311
+ // single fragment
312
+ this.#fragmentsBytes = 0
313
+ return fragments.shift()
271
314
  }
272
315
 
273
- const buffer = Buffer.allocUnsafe(n)
274
316
  let offset = 0
317
+ // If Buffer.allocUnsafe is used, extra copies will be made because the offset is non-zero.
318
+ const output = Buffer.allocUnsafeSlow(this.#fragmentsBytes)
275
319
 
276
- while (offset !== n) {
277
- const next = this.#buffers[0]
278
- const { length } = next
279
-
280
- if (length + offset === n) {
281
- buffer.set(this.#buffers.shift(), offset)
282
- break
283
- } else if (length + offset > n) {
284
- buffer.set(next.subarray(0, n - offset), offset)
285
- this.#buffers[0] = next.subarray(n - offset)
286
- break
287
- } else {
288
- buffer.set(this.#buffers.shift(), offset)
289
- offset += next.length
290
- }
320
+ for (let i = 0; i < fragments.length; ++i) {
321
+ const buffer = fragments[i]
322
+ output.set(buffer, offset)
323
+ offset += buffer.length
291
324
  }
292
325
 
293
- this.#byteOffset -= n
326
+ this.#fragments = []
327
+ this.#fragmentsBytes = 0
294
328
 
295
- return buffer
329
+ return output
296
330
  }
297
331
 
298
332
  parseCloseBody (data) {
@@ -3,8 +3,8 @@
3
3
  const { createDeferredPromise, environmentSettingsObject } = require('../../fetch/util')
4
4
  const { states, opcodes, sentCloseFrameState } = require('../constants')
5
5
  const { webidl } = require('../../fetch/webidl')
6
- const { getURLRecord, isValidSubprotocol, isEstablished, failWebsocketConnection, utf8Decode } = require('../util')
7
- const { establishWebSocketConnection, closeWebSocketConnection } = require('../connection')
6
+ const { getURLRecord, isValidSubprotocol, isEstablished, utf8Decode } = require('../util')
7
+ const { establishWebSocketConnection, failWebsocketConnection, closeWebSocketConnection } = require('../connection')
8
8
  const { types } = require('node:util')
9
9
  const { channels } = require('../../../core/diagnostics')
10
10
  const { WebsocketFrameSend } = require('../frame')
@@ -87,7 +87,7 @@ function toArrayBuffer (buffer) {
87
87
  if (buffer.byteLength === buffer.buffer.byteLength) {
88
88
  return buffer.buffer
89
89
  }
90
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
90
+ return new Uint8Array(buffer).buffer
91
91
  }
92
92
 
93
93
  /**
@@ -156,32 +156,6 @@ function isValidStatusCode (code) {
156
156
  return code >= 3000 && code <= 4999
157
157
  }
158
158
 
159
- /**
160
- * @param {import('./websocket').Handler} handler
161
- * @param {number} code
162
- * @param {string|undefined} reason
163
- * @returns {void}
164
- */
165
- function failWebsocketConnection (handler, code, reason) {
166
- // If _The WebSocket Connection is Established_ prior to the point where
167
- // the endpoint is required to _Fail the WebSocket Connection_, the
168
- // endpoint SHOULD send a Close frame with an appropriate status code
169
- // (Section 7.4) before proceeding to _Close the WebSocket Connection_.
170
- if (isEstablished(handler.readyState)) {
171
- // avoid circular require - performance is not important here
172
- const { closeWebSocketConnection } = require('./connection')
173
- closeWebSocketConnection(handler, code, reason, false)
174
- }
175
-
176
- handler.controller.abort()
177
-
178
- if (handler.socket?.destroyed === false) {
179
- handler.socket.destroy()
180
- }
181
-
182
- handler.onFail(code, reason)
183
- }
184
-
185
159
  /**
186
160
  * @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.5
187
161
  * @param {number} opcode
@@ -350,7 +324,6 @@ module.exports = {
350
324
  fireEvent,
351
325
  isValidSubprotocol,
352
326
  isValidStatusCode,
353
- failWebsocketConnection,
354
327
  websocketMessageReceived,
355
328
  utf8Decode,
356
329
  isControlFrame,
@@ -10,12 +10,11 @@ const {
10
10
  isClosing,
11
11
  isValidSubprotocol,
12
12
  fireEvent,
13
- failWebsocketConnection,
14
13
  utf8Decode,
15
14
  toArrayBuffer,
16
15
  getURLRecord
17
16
  } = require('./util')
18
- const { establishWebSocketConnection, closeWebSocketConnection } = require('./connection')
17
+ const { establishWebSocketConnection, closeWebSocketConnection, failWebsocketConnection } = require('./connection')
19
18
  const { ByteParser } = require('./receiver')
20
19
  const { kEnumerableProperty } = require('../../core/util')
21
20
  const { getGlobalDispatcher } = require('../../global')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "7.2.0",
3
+ "version": "7.2.2",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {
@@ -107,7 +107,7 @@
107
107
  "prepare": "husky && node ./scripts/platform-shell.js"
108
108
  },
109
109
  "devDependencies": {
110
- "@fastify/busboy": "3.1.0",
110
+ "@fastify/busboy": "3.1.1",
111
111
  "@matteo.collina/tspl": "^0.1.1",
112
112
  "@sinonjs/fake-timers": "^12.0.0",
113
113
  "@types/node": "^18.19.50",
package/types/errors.d.ts CHANGED
@@ -33,6 +33,22 @@ declare namespace Errors {
33
33
  code: 'UND_ERR_BODY_TIMEOUT'
34
34
  }
35
35
 
36
+ export class ResponseError extends UndiciError {
37
+ constructor (
38
+ message: string,
39
+ code: number,
40
+ options: {
41
+ headers?: IncomingHttpHeaders | string[] | null,
42
+ body?: null | Record<string, any> | string
43
+ }
44
+ )
45
+ name: 'ResponseError'
46
+ code: 'UND_ERR_RESPONSE'
47
+ statusCode: number
48
+ body: null | Record<string, any> | string
49
+ headers: IncomingHttpHeaders | string[] | null
50
+ }
51
+
36
52
  export class ResponseStatusCodeError extends UndiciError {
37
53
  constructor (
38
54
  message?: string,