undici 8.11.0 → 8.11.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.
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
 
@@ -700,6 +700,11 @@ function _resume (client, sync) {
700
700
  return
701
701
  }
702
702
 
703
+ if (request.aborted) {
704
+ client[kQueue].splice(client[kPendingIdx], 1)
705
+ continue
706
+ }
707
+
703
708
  if (client[kUrl].protocol === 'https:' && client[kServerName] !== request.servername) {
704
709
  if (client[kRunning] > 0) {
705
710
  return
@@ -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
  },
@@ -101,13 +101,13 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
101
101
  // The presence of a session property on the socket indicates HTTP2
102
102
  // HTTP1
103
103
  if (response.socket?.session == null) {
104
- failWebsocketConnection(handler, 1002, 'Received network error or non-101 status code.', response.error)
104
+ failHandshake(handler, response, 1002, 'Received network error or non-101 status code.', response.error)
105
105
  return
106
106
  }
107
107
 
108
108
  // HTTP2
109
109
  if (response.status !== 200) {
110
- failWebsocketConnection(handler, 1002, 'Received network error or non-200 status code.', response.error)
110
+ failHandshake(handler, response, 1002, 'Received network error or non-200 status code.', response.error)
111
111
  return
112
112
  }
113
113
  }
@@ -122,7 +122,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
122
122
  // header list results in null, failure, or the empty byte
123
123
  // sequence, then fail the WebSocket connection.
124
124
  if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) {
125
- failWebsocketConnection(handler, 1002, 'Server did not respond with sent protocols.')
125
+ failHandshake(handler, response, 1002, 'Server did not respond with sent protocols.')
126
126
  return
127
127
  }
128
128
 
@@ -138,7 +138,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
138
138
  // _Fail the WebSocket Connection_.
139
139
  // For H2, no upgrade header is expected.
140
140
  if (response.socket.session == null && response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') {
141
- failWebsocketConnection(handler, 1002, 'Server did not set Upgrade header to "websocket".')
141
+ failHandshake(handler, response, 1002, 'Server did not set Upgrade header to "websocket".')
142
142
  return
143
143
  }
144
144
 
@@ -148,7 +148,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
148
148
  // MUST _Fail the WebSocket Connection_.
149
149
  // For H2, no connection header is expected.
150
150
  if (response.socket.session == null && response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') {
151
- failWebsocketConnection(handler, 1002, 'Server did not set Connection header to "upgrade".')
151
+ failHandshake(handler, response, 1002, 'Server did not set Connection header to "upgrade".')
152
152
  return
153
153
  }
154
154
 
@@ -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
+ failHandshake(handler, response, 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
@@ -180,7 +184,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
180
184
  extensions = parseExtensions(secExtension)
181
185
 
182
186
  if (!extensions.has('permessage-deflate')) {
183
- failWebsocketConnection(handler, 1002, 'Sec-WebSocket-Extensions header does not match.')
187
+ failHandshake(handler, response, 1002, 'Sec-WebSocket-Extensions header does not match.')
184
188
  return
185
189
  }
186
190
  }
@@ -201,11 +205,18 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
201
205
  // the selected subprotocol values in its response for the connection to
202
206
  // be established.
203
207
  if (requestProtocols === null || !requestProtocols.includes(secProtocol)) {
204
- failWebsocketConnection(handler, 1002, 'Protocol was not set in the opening handshake.')
208
+ failHandshake(handler, response, 1002, 'Protocol was not set in the opening handshake.')
205
209
  return
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)
@@ -296,6 +307,16 @@ function closeWebSocketConnection (object, code, reason, validate = false) {
296
307
  }
297
308
  }
298
309
 
310
+ function failHandshake (handler, response, code, reason, cause) {
311
+ // The H2 upgrade request has already completed and handed off its stream.
312
+ // Aborting the request cannot close that stream after handshake validation fails.
313
+ if (response.socket?.session != null && !response.socket.destroyed) {
314
+ response.socket.destroy()
315
+ }
316
+
317
+ failWebsocketConnection(handler, code, reason, cause)
318
+ }
319
+
299
320
  /**
300
321
  * @param {import('./websocket').Handler} handler
301
322
  * @param {number} code
@@ -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.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": {