undici 8.11.0 → 8.11.1

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
@@ -378,7 +378,16 @@ The `body` mixins are the most common way to format the request/response body. M
378
378
  > The body returned from `undici.request` does not implement `.formData()`.
379
379
 
380
380
  > [!WARNING]
381
- > Calling `body.formData()` on a fetch response causes undici to buffer and parse the entire body. Since this is dictated by the spec, `body.formData()` must only be called on responses from trusted servers.
381
+ > The body mixins `.arrayBuffer()`, `.blob()`, `.bytes()`, `.json()`, `.text()`,
382
+ > and `.formData()` buffer the entire body in memory before returning. Where
383
+ > applicable, they also decode or parse the payload and retain that
384
+ > representation in memory. Calling these methods therefore means trusting that
385
+ > the response body is small enough to fit in the available memory. Do not use
386
+ > them for responses from untrusted or user-controlled sources. Instead, consume
387
+ > the response body as a stream and enforce an application-specific size limit:
388
+ > use `response.body` for fetch responses or the `body` returned by
389
+ > `undici.request()`. For streaming decoded text, use `body.textStream()` on a
390
+ > fetch `Request` or `Response`.
382
391
 
383
392
  Example usage:
384
393
 
@@ -466,6 +466,15 @@ example, calling `text()` after `json()` throws a `TypeError`. The body also
466
466
  provides `dump({ limit })`, which discards up to `limit` bytes (default
467
467
  `131072`) without destroying the socket.
468
468
 
469
+ > [!WARNING]
470
+ > The `arrayBuffer()`, `blob()`, `bytes()`, `json()`, and `text()` methods buffer
471
+ > the entire body in memory before returning. Where applicable, they also decode
472
+ > or parse the payload and retain that representation in memory. Calling these
473
+ > methods therefore means trusting that the body is small enough to fit in the
474
+ > available memory. Do not use them for bodies received from untrusted or
475
+ > user-controlled sources. Instead, process `body` as a `Readable` stream and
476
+ > enforce an application-specific size limit.
477
+
469
478
  The body is always a `Readable`, even when empty. Deserializing an empty body
470
479
  with `json()` throws. To guard against this, verify the status code is not `204`
471
480
  and the `content-type` header starts with `application/json` before calling
@@ -596,6 +596,16 @@ properties for reading a body. Each consuming method reads the body once; after
596
596
  the body has been consumed, [`bodyUsed`](#bodybodyused) becomes `true` and
597
597
  calling another consuming method throws a `TypeError`.
598
598
 
599
+ > [!WARNING]
600
+ > The `arrayBuffer()`, `blob()`, `bytes()`, `formData()`, `json()`, and `text()`
601
+ > methods buffer the entire body in memory before returning. Where applicable,
602
+ > they also decode or parse the payload and retain that representation in
603
+ > memory. Calling these methods therefore means trusting that the body is small
604
+ > enough to fit in the available memory. Do not use them for bodies received
605
+ > from untrusted or user-controlled sources. Instead, process `body.body` or
606
+ > `body.textStream()` incrementally and enforce an application-specific size
607
+ > limit.
608
+
599
609
  ### `body.arrayBuffer()`
600
610
 
601
611
  <!-- YAML
@@ -380,7 +380,16 @@ The `body` mixins are the most common way to format the request/response body. M
380
380
  > The body returned from `undici.request` does not implement `.formData()`.
381
381
 
382
382
  > [!WARNING]
383
- > Calling `body.formData()` on a fetch response causes undici to buffer and parse the entire body. Since this is dictated by the spec, `body.formData()` must only be called on responses from trusted servers.
383
+ > The body mixins `.arrayBuffer()`, `.blob()`, `.bytes()`, `.json()`, `.text()`,
384
+ > and `.formData()` buffer the entire body in memory before returning. Where
385
+ > applicable, they also decode or parse the payload and retain that
386
+ > representation in memory. Calling these methods therefore means trusting that
387
+ > the response body is small enough to fit in the available memory. Do not use
388
+ > them for responses from untrusted or user-controlled sources. Instead, consume
389
+ > the response body as a stream and enforce an application-specific size limit:
390
+ > use `response.body` for fetch responses or the `body` returned by
391
+ > `undici.request()`. For streaming decoded text, use `body.textStream()` on a
392
+ > fetch `Request` or `Response`.
384
393
 
385
394
  Example usage:
386
395
 
@@ -89,10 +89,9 @@ class Dispatcher1Wrapper extends Dispatcher {
89
89
  }
90
90
 
91
91
  dispatch (opts, handler) {
92
- // Legacy (v1) WebSocket consumers do not support falling back to HTTP/1.1
93
- // when HTTP/2 Extended CONNECT is unavailable.
92
+ // Legacy (v1) consumers do not support HTTP/2, so force HTTP/1.1.
94
93
  // See https://github.com/nodejs/undici/issues/4989
95
- if (opts.upgrade && opts.allowH2 !== false) {
94
+ if (opts.allowH2 !== false) {
96
95
  opts = { ...opts, allowH2: false }
97
96
  }
98
97
 
@@ -812,11 +812,8 @@ async function mainFetch (fetchParams, recursive) {
812
812
  // https://fetch.spec.whatwg.org/#concept-scheme-fetch
813
813
  // given a fetch params fetchParams
814
814
  function schemeFetch (fetchParams) {
815
- // Note: since the connection is destroyed on redirect, which sets fetchParams to a
816
- // cancelled state, we do not want this condition to trigger *unless* there have been
817
- // no redirects. See https://github.com/nodejs/undici/issues/1776
818
815
  // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams.
819
- if (isCancelled(fetchParams) && fetchParams.request.redirectCount === 0) {
816
+ if (isCancelled(fetchParams)) {
820
817
  return Promise.resolve(makeAppropriateNetworkError(fetchParams))
821
818
  }
822
819
 
@@ -1243,7 +1240,7 @@ async function httpFetch (fetchParams) {
1243
1240
  // encouraged to, transmit an RST_STREAM frame.
1244
1241
  // See, https://github.com/whatwg/fetch/issues/1288
1245
1242
  if (request.redirect !== 'manual') {
1246
- fetchParams.controller.connection.destroy(undefined, false)
1243
+ fetchParams.controller.connection.destroy()
1247
1244
  }
1248
1245
 
1249
1246
  // 2. Switch on request’s redirect mode:
@@ -1852,12 +1849,10 @@ async function httpNetworkFetch (
1852
1849
  fetchParams.controller.connection = {
1853
1850
  abort: null,
1854
1851
  destroyed: false,
1855
- destroy (err, abort = true) {
1852
+ destroy (err) {
1856
1853
  if (!this.destroyed) {
1857
1854
  this.destroyed = true
1858
- if (abort) {
1859
- this.abort?.(err ?? new DOMException('The operation was aborted.', 'AbortError'))
1860
- }
1855
+ this.abort?.(err ?? new DOMException('The operation was aborted.', 'AbortError'))
1861
1856
  }
1862
1857
  }
1863
1858
  }
@@ -2393,7 +2388,7 @@ async function httpNetworkFetch (
2393
2388
  this.body?.push(null)
2394
2389
  },
2395
2390
 
2396
- onResponseError (_controller, error) {
2391
+ onResponseError (controller, error) {
2397
2392
  if (this.abort) {
2398
2393
  fetchParams.controller.off('terminated', this.abort)
2399
2394
  }
@@ -2412,7 +2407,9 @@ async function httpNetworkFetch (
2412
2407
 
2413
2408
  this.body?.destroy(error)
2414
2409
 
2415
- fetchParams.controller.terminate(error)
2410
+ if (!controller?.aborted) {
2411
+ fetchParams.controller.terminate(error)
2412
+ }
2416
2413
 
2417
2414
  reject(error)
2418
2415
  },
@@ -159,11 +159,15 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
159
159
  // E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and
160
160
  // trailing whitespace, the client MUST _Fail the WebSocket
161
161
  // Connection_.
162
- const secWSAccept = response.headersList.get('Sec-WebSocket-Accept')
163
- const digest = crypto.hash('sha1', keyValue + uid, 'base64')
164
- if (secWSAccept !== digest) {
165
- failWebsocketConnection(handler, 1002, 'Incorrect hash received in Sec-WebSocket-Accept header.')
166
- return
162
+ // For H2, implementations "do not do the processing of the Sec-WebSocket-Key and
163
+ // Sec-WebSocket-Accept header fields". https://datatracker.ietf.org/doc/html/rfc8441#section-5
164
+ if (response.socket.session == null) {
165
+ const secWSAccept = response.headersList.get('Sec-WebSocket-Accept')
166
+ const digest = crypto.hash('sha1', keyValue + uid, 'base64')
167
+ if (secWSAccept !== digest) {
168
+ failWebsocketConnection(handler, 1002, 'Incorrect hash received in Sec-WebSocket-Accept header.')
169
+ return
170
+ }
167
171
  }
168
172
 
169
173
  // 5. If the response includes a |Sec-WebSocket-Extensions| header
@@ -206,6 +210,13 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
206
210
  }
207
211
  }
208
212
 
213
+ // For H2, "Orderly TCP-level closures are represented as END_STREAM flags", so
214
+ // end our side of the stream when the server ends its side, as a TCP socket would.
215
+ // https://datatracker.ietf.org/doc/html/rfc8441#section-5
216
+ if (response.socket.session != null) {
217
+ response.socket.allowHalfOpen = false
218
+ }
219
+
209
220
  response.socket.on('data', handler.onSocketData)
210
221
  response.socket.on('close', handler.onSocketClose)
211
222
  response.socket.on('error', handler.onSocketError)
@@ -369,9 +369,6 @@ class ByteParser extends Writable {
369
369
  consumeFragments () {
370
370
  const fragments = this.#fragments
371
371
 
372
- // The compressed flag describes the message that is being completed here.
373
- // It must not survive into the next message, otherwise a stray
374
- // continuation frame is accepted instead of failing the connection.
375
372
  this.#info.compressed = false
376
373
 
377
374
  if (fragments.length === 1) {
@@ -62,7 +62,9 @@ class WebSocketStream {
62
62
 
63
63
  this.#handler.socket.destroy()
64
64
  },
65
- onSocketClose: () => this.#onSocketClose(),
65
+ // When the WebSocket connection is closed for a WebSocketStream stream, possibly cleanly, the user agent must
66
+ // queue a global task on the WebSocket task source given stream ’s relevant global object to run the following substeps:
67
+ onSocketClose: () => queueMicrotask(() => this.#onSocketClose()),
66
68
  onPing: () => {},
67
69
  onPong: () => {},
68
70
 
@@ -246,11 +248,15 @@ class WebSocketStream {
246
248
  const frame = new WebsocketFrameSend(data)
247
249
 
248
250
  this.#handler.socket.write(frame.createFrame(opcode), () => {
251
+ // 6.3. Queue a global task on the WebSocket task source given stream ’s relevant global object to resolve promise with undefined.
249
252
  promise.resolve(undefined)
250
253
  })
254
+ } else {
255
+ // 6.3. Queue a global task on the WebSocket task source given stream ’s relevant global object to resolve promise with undefined.
256
+ promise.resolve(undefined)
251
257
  }
252
258
 
253
- // 6.3. Queue a global task on the WebSocket task source given stream ’s relevant global object to resolve promise with undefined.
259
+ // 7. Return promise.
254
260
  return promise.promise
255
261
  }
256
262
 
@@ -278,7 +284,7 @@ class WebSocketStream {
278
284
  // This is done in the opening handshake.
279
285
 
280
286
  // 3. Let extensions be the extensions in use .
281
- const extensions = parsedExtensions ?? ''
287
+ const extensions = response.headersList.get('sec-websocket-extensions') ?? ''
282
288
 
283
289
  // 4. Let protocol be the subprotocol in use .
284
290
  const protocol = response.headersList.get('sec-websocket-protocol') ?? ''
@@ -291,6 +297,7 @@ class WebSocketStream {
291
297
  start: (controller) => {
292
298
  this.#readableStreamController = controller
293
299
  },
300
+ pull: () => this.#pull(),
294
301
  cancel: (reason) => this.#cancel(reason)
295
302
  })
296
303
 
@@ -350,6 +357,9 @@ class WebSocketStream {
350
357
  this.#readableStreamController.enqueue(chunk)
351
358
 
352
359
  // 4. Apply backpressure to the WebSocket.
360
+ if (this.#readableStreamController.desiredSize <= 0) {
361
+ this.#handler.socket.pause()
362
+ }
353
363
  }
354
364
 
355
365
  /** @type {import('../websocket').Handler['onSocketClose']} */
@@ -383,7 +393,7 @@ class WebSocketStream {
383
393
  // 1006.
384
394
  let code = result?.code ?? 1005
385
395
 
386
- if (!this.#handler.closeState.has(sentCloseFrameState.SENT) && !this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
396
+ if (!this.#handler.closeState.has(sentCloseFrameState.RECEIVED)) {
387
397
  code = 1006
388
398
  }
389
399
 
@@ -441,6 +451,11 @@ class WebSocketStream {
441
451
  closeWebSocketConnection(this.#handler, code, reasonString)
442
452
  }
443
453
 
454
+ // To pull bytes from a WebSocketStream stream , if stream is currently applying backpressure, release backpressure.
455
+ #pull () {
456
+ this.#handler.socket.resume()
457
+ }
458
+
444
459
  // To cancel a WebSocketStream stream given reason , close using reason giving stream and reason .
445
460
  #cancel (reason) {
446
461
  this.#closeUsingReason(reason)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "8.11.0",
3
+ "version": "8.11.1",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {