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 +10 -1
- package/docs/docs/api/Dispatcher.md +9 -0
- package/docs/docs/api/Fetch.md +10 -0
- package/docs/docs/index.md +10 -1
- package/lib/dispatcher/client.js +5 -0
- package/lib/dispatcher/dispatcher1-wrapper.js +2 -3
- package/lib/web/fetch/index.js +8 -11
- package/lib/web/websocket/connection.js +33 -12
- package/lib/web/websocket/receiver.js +0 -3
- package/lib/web/websocket/stream/websocketstream.js +19 -4
- package/package.json +1 -1
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
|
-
>
|
|
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
|
package/docs/docs/api/Fetch.md
CHANGED
|
@@ -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
|
package/docs/docs/index.md
CHANGED
|
@@ -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
|
-
>
|
|
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
|
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -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)
|
|
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.
|
|
94
|
+
if (opts.allowH2 !== false) {
|
|
96
95
|
opts = { ...opts, allowH2: false }
|
|
97
96
|
}
|
|
98
97
|
|
package/lib/web/fetch/index.js
CHANGED
|
@@ -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)
|
|
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(
|
|
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
|
|
1852
|
+
destroy (err) {
|
|
1856
1853
|
if (!this.destroyed) {
|
|
1857
1854
|
this.destroyed = true
|
|
1858
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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 =
|
|
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.
|
|
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)
|