undici 8.0.3 → 8.2.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.
Files changed (54) hide show
  1. package/docs/docs/api/Client.md +2 -0
  2. package/docs/docs/api/Dispatcher.md +2 -2
  3. package/lib/api/api-connect.js +1 -1
  4. package/lib/api/api-pipeline.js +2 -2
  5. package/lib/api/api-request.js +2 -2
  6. package/lib/api/api-stream.js +1 -1
  7. package/lib/api/api-upgrade.js +8 -2
  8. package/lib/api/readable.js +3 -2
  9. package/lib/cache/memory-cache-store.js +1 -1
  10. package/lib/cache/sqlite-cache-store.js +6 -4
  11. package/lib/core/connect.js +16 -0
  12. package/lib/core/constants.js +1 -24
  13. package/lib/core/errors.js +2 -2
  14. package/lib/core/request.js +17 -2
  15. package/lib/core/socks5-client.js +24 -9
  16. package/lib/core/socks5-utils.js +32 -23
  17. package/lib/core/util.js +28 -3
  18. package/lib/dispatcher/agent.js +38 -40
  19. package/lib/dispatcher/balanced-pool.js +21 -23
  20. package/lib/dispatcher/client-h1.js +34 -16
  21. package/lib/dispatcher/client-h2.js +400 -147
  22. package/lib/dispatcher/client.js +3 -2
  23. package/lib/dispatcher/dispatcher-base.js +18 -0
  24. package/lib/dispatcher/h2c-client.js +4 -4
  25. package/lib/dispatcher/pool-base.js +6 -6
  26. package/lib/dispatcher/pool.js +8 -3
  27. package/lib/dispatcher/proxy-agent.js +2 -0
  28. package/lib/dispatcher/round-robin-pool.js +5 -6
  29. package/lib/dispatcher/socks5-proxy-agent.js +23 -14
  30. package/lib/handler/cache-handler.js +1 -1
  31. package/lib/handler/redirect-handler.js +4 -0
  32. package/lib/interceptor/redirect.js +3 -3
  33. package/lib/llhttp/llhttp-wasm.js +1 -1
  34. package/lib/llhttp/llhttp_simd-wasm.js +1 -1
  35. package/lib/mock/mock-agent.js +8 -8
  36. package/lib/mock/mock-call-history.js +15 -15
  37. package/lib/util/cache.js +1 -1
  38. package/lib/web/eventsource/eventsource-stream.js +245 -150
  39. package/lib/web/fetch/formdata-parser.js +17 -6
  40. package/lib/web/fetch/index.js +38 -28
  41. package/lib/web/webidl/index.js +5 -5
  42. package/lib/web/websocket/frame.js +1 -7
  43. package/lib/web/websocket/permessage-deflate.js +13 -31
  44. package/lib/web/websocket/receiver.js +62 -22
  45. package/lib/web/websocket/stream/websocketstream.js +6 -5
  46. package/lib/web/websocket/websocket.js +6 -1
  47. package/package.json +1 -1
  48. package/types/client.d.ts +11 -0
  49. package/types/dispatcher.d.ts +4 -4
  50. package/types/header.d.ts +5 -0
  51. package/types/interceptors.d.ts +1 -1
  52. package/types/proxy-agent.d.ts +2 -2
  53. package/types/socks5-proxy-agent.d.ts +2 -2
  54. package/lib/llhttp/.gitkeep +0 -0
@@ -10,13 +10,7 @@ let bufIdx = BUFFER_SIZE
10
10
 
11
11
  const randomFillSync = runtimeFeatures.has('crypto')
12
12
  ? require('node:crypto').randomFillSync
13
- // not full compatibility, but minimum.
14
- : function randomFillSync (buffer, _offset, _size) {
15
- for (let i = 0; i < buffer.length; ++i) {
16
- buffer[i] = Math.random() * 255 | 0
17
- }
18
- return buffer
19
- }
13
+ : null
20
14
 
21
15
  function generateMask () {
22
16
  if (bufIdx === BUFFER_SIZE) {
@@ -8,40 +8,35 @@ const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
8
8
  const kBuffer = Symbol('kBuffer')
9
9
  const kLength = Symbol('kLength')
10
10
 
11
- // Default maximum decompressed message size: 4 MB
12
- const kDefaultMaxDecompressedSize = 4 * 1024 * 1024
13
-
14
11
  class PerMessageDeflate {
15
12
  /** @type {import('node:zlib').InflateRaw} */
16
13
  #inflate
17
14
 
18
15
  #options = {}
19
16
 
20
- /** @type {boolean} */
21
- #aborted = false
22
-
23
- /** @type {Function|null} */
24
- #currentCallback = null
17
+ #maxPayloadSize = 0
25
18
 
26
19
  /**
27
20
  * @param {Map<string, string>} extensions
28
21
  */
29
- constructor (extensions) {
22
+ constructor (extensions, options) {
30
23
  this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
31
24
  this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
25
+
26
+ this.#maxPayloadSize = options.maxPayloadSize
32
27
  }
33
28
 
29
+ /**
30
+ * Decompress a compressed payload.
31
+ * @param {Buffer} chunk Compressed data
32
+ * @param {boolean} fin Final fragment flag
33
+ * @param {Function} callback Callback function
34
+ */
34
35
  decompress (chunk, fin, callback) {
35
36
  // An endpoint uses the following algorithm to decompress a message.
36
37
  // 1. Append 4 octets of 0x00 0x00 0xff 0xff to the tail end of the
37
38
  // payload of the message.
38
39
  // 2. Decompress the resulting data using DEFLATE.
39
-
40
- if (this.#aborted) {
41
- callback(new MessageSizeExceededError())
42
- return
43
- }
44
-
45
40
  if (!this.#inflate) {
46
41
  let windowBits = Z_DEFAULT_WINDOWBITS
47
42
 
@@ -64,23 +59,12 @@ class PerMessageDeflate {
64
59
  this.#inflate[kLength] = 0
65
60
 
66
61
  this.#inflate.on('data', (data) => {
67
- if (this.#aborted) {
68
- return
69
- }
70
-
71
62
  this.#inflate[kLength] += data.length
72
63
 
73
- if (this.#inflate[kLength] > kDefaultMaxDecompressedSize) {
74
- this.#aborted = true
64
+ if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) {
65
+ callback(new MessageSizeExceededError())
75
66
  this.#inflate.removeAllListeners()
76
- this.#inflate.destroy()
77
67
  this.#inflate = null
78
-
79
- if (this.#currentCallback) {
80
- const cb = this.#currentCallback
81
- this.#currentCallback = null
82
- cb(new MessageSizeExceededError())
83
- }
84
68
  return
85
69
  }
86
70
 
@@ -93,14 +77,13 @@ class PerMessageDeflate {
93
77
  })
94
78
  }
95
79
 
96
- this.#currentCallback = callback
97
80
  this.#inflate.write(chunk)
98
81
  if (fin) {
99
82
  this.#inflate.write(tail)
100
83
  }
101
84
 
102
85
  this.#inflate.flush(() => {
103
- if (this.#aborted || !this.#inflate) {
86
+ if (!this.#inflate) {
104
87
  return
105
88
  }
106
89
 
@@ -108,7 +91,6 @@ class PerMessageDeflate {
108
91
 
109
92
  this.#inflate[kBuffer].length = 0
110
93
  this.#inflate[kLength] = 0
111
- this.#currentCallback = null
112
94
 
113
95
  callback(null, full)
114
96
  })
@@ -39,18 +39,23 @@ class ByteParser extends Writable {
39
39
  /** @type {import('./websocket').Handler} */
40
40
  #handler
41
41
 
42
+ /** @type {number} */
43
+ #maxPayloadSize
44
+
42
45
  /**
43
46
  * @param {import('./websocket').Handler} handler
44
47
  * @param {Map<string, string>|null} extensions
48
+ * @param {{ maxPayloadSize?: number }} [options]
45
49
  */
46
- constructor (handler, extensions) {
50
+ constructor (handler, extensions, options = {}) {
47
51
  super()
48
52
 
49
53
  this.#handler = handler
50
54
  this.#extensions = extensions == null ? new Map() : extensions
55
+ this.#maxPayloadSize = options.maxPayloadSize ?? 0
51
56
 
52
57
  if (this.#extensions.has('permessage-deflate')) {
53
- this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions))
58
+ this.#extensions.set('permessage-deflate', new PerMessageDeflate(extensions, options))
54
59
  }
55
60
  }
56
61
 
@@ -66,6 +71,19 @@ class ByteParser extends Writable {
66
71
  this.run(callback)
67
72
  }
68
73
 
74
+ #validatePayloadLength () {
75
+ if (
76
+ this.#maxPayloadSize > 0 &&
77
+ !isControlFrame(this.#info.opcode) &&
78
+ this.#info.payloadLength > this.#maxPayloadSize
79
+ ) {
80
+ failWebsocketConnection(this.#handler, 1009, 'Payload size exceeds maximum allowed size')
81
+ return false
82
+ }
83
+
84
+ return true
85
+ }
86
+
69
87
  /**
70
88
  * Runs whenever a new chunk is received.
71
89
  * Callback is called whenever there are no more chunks buffering,
@@ -154,6 +172,10 @@ class ByteParser extends Writable {
154
172
  if (payloadLength <= 125) {
155
173
  this.#info.payloadLength = payloadLength
156
174
  this.#state = parserStates.READ_DATA
175
+
176
+ if (!this.#validatePayloadLength()) {
177
+ return
178
+ }
157
179
  } else if (payloadLength === 126) {
158
180
  this.#state = parserStates.PAYLOADLENGTH_16
159
181
  } else if (payloadLength === 127) {
@@ -178,6 +200,10 @@ class ByteParser extends Writable {
178
200
 
179
201
  this.#info.payloadLength = buffer.readUInt16BE(0)
180
202
  this.#state = parserStates.READ_DATA
203
+
204
+ if (!this.#validatePayloadLength()) {
205
+ return
206
+ }
181
207
  } else if (this.#state === parserStates.PAYLOADLENGTH_64) {
182
208
  if (this.#byteOffset < 8) {
183
209
  return callback()
@@ -200,6 +226,10 @@ class ByteParser extends Writable {
200
226
 
201
227
  this.#info.payloadLength = lower
202
228
  this.#state = parserStates.READ_DATA
229
+
230
+ if (!this.#validatePayloadLength()) {
231
+ return
232
+ }
203
233
  } else if (this.#state === parserStates.READ_DATA) {
204
234
  if (this.#byteOffset < this.#info.payloadLength) {
205
235
  return callback()
@@ -224,29 +254,39 @@ class ByteParser extends Writable {
224
254
 
225
255
  this.#state = parserStates.INFO
226
256
  } else {
227
- this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
228
- if (error) {
229
- // Use 1009 (Message Too Big) for decompression size limit errors
230
- const code = error instanceof MessageSizeExceededError ? 1009 : 1007
231
- failWebsocketConnection(this.#handler, code, error.message)
232
- return
233
- }
234
-
235
- this.writeFragments(data)
257
+ this.#extensions.get('permessage-deflate').decompress(
258
+ body,
259
+ this.#info.fin,
260
+ (error, data) => {
261
+ if (error) {
262
+ const code = error instanceof MessageSizeExceededError ? 1009 : 1007
263
+ failWebsocketConnection(this.#handler, code, error.message)
264
+ return
265
+ }
266
+
267
+ this.writeFragments(data)
268
+
269
+ // Check cumulative fragment size
270
+ if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
271
+ failWebsocketConnection(this.#handler, 1009, new MessageSizeExceededError().message)
272
+ return
273
+ }
274
+
275
+ if (!this.#info.fin) {
276
+ this.#state = parserStates.INFO
277
+ this.#loop = true
278
+ this.run(callback)
279
+ return
280
+ }
281
+
282
+ websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments())
236
283
 
237
- if (!this.#info.fin) {
238
- this.#state = parserStates.INFO
239
284
  this.#loop = true
285
+ this.#state = parserStates.INFO
240
286
  this.run(callback)
241
- return
242
- }
243
-
244
- websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments())
245
-
246
- this.#loop = true
247
- this.#state = parserStates.INFO
248
- this.run(callback)
249
- })
287
+ },
288
+ this.#fragmentsBytes
289
+ )
250
290
 
251
291
  this.#loop = false
252
292
  break
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
- const { environmentSettingsObject } = require('../../fetch/util')
3
+ const { addAbortListener } = require('node:events')
4
+ const { environmentSettingsObject, readableStreamClose } = require('../../fetch/util')
4
5
  const { states, opcodes, sentCloseFrameState } = require('../constants')
5
6
  const { webidl } = require('../../webidl')
6
7
  const { getURLRecord, isValidSubprotocol, isEstablished, utf8Decode } = require('../util')
@@ -132,7 +133,7 @@ class WebSocketStream {
132
133
  }
133
134
 
134
135
  // 8.3. Add the following abort steps to signal :
135
- signal.addEventListener('abort', () => {
136
+ addAbortListener(signal, () => {
136
137
  // 8.3.1. If the WebSocket connection is not yet established : [WSP]
137
138
  if (!isEstablished(this.#handler.readyState)) {
138
139
  // 8.3.1.1. Fail the WebSocket connection .
@@ -148,7 +149,7 @@ class WebSocketStream {
148
149
  // Set this 's handshake aborted to true.
149
150
  this.#handshakeAborted = true
150
151
  }
151
- }, { once: true })
152
+ })
152
153
  }
153
154
 
154
155
  // 9. Let client be this 's relevant settings object .
@@ -331,7 +332,7 @@ class WebSocketStream {
331
332
  try {
332
333
  chunk = utf8Decode(data)
333
334
  } catch {
334
- failWebsocketConnection(this.#handler, 'Received invalid UTF-8 in text frame.')
335
+ failWebsocketConnection(this.#handler, 1007, 'Received invalid UTF-8 in text frame.')
335
336
  return
336
337
  }
337
338
  } else if (type === opcodes.BINARY) {
@@ -385,7 +386,7 @@ class WebSocketStream {
385
386
  // 6. If the connection was closed cleanly ,
386
387
  if (wasClean) {
387
388
  // 6.1. Close stream ’s readable stream .
388
- this.#readableStreamController.close()
389
+ readableStreamClose(this.#readableStreamController)
389
390
 
390
391
  // 6.2. Error stream ’s writable stream with an " InvalidStateError " DOMException indicating that a closed WebSocketStream cannot be written to.
391
392
  if (!this.#writableStream.locked) {
@@ -468,7 +468,12 @@ class WebSocket extends EventTarget {
468
468
  // once this happens, the connection is open
469
469
  this.#handler.socket = response.socket
470
470
 
471
- const parser = new ByteParser(this.#handler, parsedExtensions)
471
+ // Get maxPayloadSize from dispatcher options
472
+ const maxPayloadSize = this.#handler.controller.dispatcher?.webSocketOptions?.maxPayloadSize
473
+
474
+ const parser = new ByteParser(this.#handler, parsedExtensions, {
475
+ maxPayloadSize
476
+ })
472
477
  parser.on('drain', () => this.#handler.onParserDrain())
473
478
  parser.on('error', (err) => this.#handler.onParserError(err))
474
479
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "8.0.3",
3
+ "version": "8.2.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": {
package/types/client.d.ts CHANGED
@@ -73,6 +73,8 @@ export declare namespace Client {
73
73
  localAddress?: string;
74
74
  /** Max response body size in bytes, -1 is disabled */
75
75
  maxResponseSize?: number;
76
+ /** WebSocket-specific options */
77
+ webSocket?: Client.WebSocketOptions;
76
78
  /** Enables a family autodetection algorithm that loosely implements section 5 of RFC 8305. */
77
79
  autoSelectFamily?: boolean;
78
80
  /** The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. */
@@ -113,6 +115,15 @@ export declare namespace Client {
113
115
  bytesWritten?: number
114
116
  bytesRead?: number
115
117
  }
118
+ export interface WebSocketOptions {
119
+ /**
120
+ * Maximum allowed payload size in bytes for WebSocket messages.
121
+ * Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages.
122
+ * Set to 0 to disable the limit.
123
+ * @default 134217728 (128 MB)
124
+ */
125
+ maxPayloadSize?: number;
126
+ }
116
127
  }
117
128
 
118
129
  export default Client
@@ -2,7 +2,7 @@ import { URL } from 'node:url'
2
2
  import { Duplex, Readable, Writable } from 'node:stream'
3
3
  import { EventEmitter } from 'node:events'
4
4
  import { Blob } from 'node:buffer'
5
- import { IncomingHttpHeaders } from './header'
5
+ import { IncomingHttpHeaders, OutgoingHttpHeaders } from './header'
6
6
  import BodyReadable from './readable'
7
7
  import { FormData } from './formdata'
8
8
  import Errors from './errors'
@@ -10,7 +10,7 @@ import { Autocomplete } from './utility'
10
10
 
11
11
  export default Dispatcher
12
12
 
13
- export type UndiciHeaders = Record<string, string | string[]> | IncomingHttpHeaders | string[] | Iterable<[string, string | string[] | undefined]> | null
13
+ export type UndiciHeaders = OutgoingHttpHeaders | string[] | Iterable<[string, string | string[] | undefined]> | null
14
14
 
15
15
  /** Dispatcher is the core API used to dispatch requests. */
16
16
  declare class Dispatcher extends EventEmitter {
@@ -210,8 +210,8 @@ declare namespace Dispatcher {
210
210
  get aborted(): boolean
211
211
  get paused(): boolean
212
212
  get reason(): Error | null
213
- rawHeaders?: Buffer[] | string[] | null
214
- rawTrailers?: Buffer[] | string[] | null
213
+ rawHeaders?: Buffer[] | string[] | IncomingHttpHeaders | null
214
+ rawTrailers?: Buffer[] | string[] | IncomingHttpHeaders | null
215
215
  abort(reason: Error): void
216
216
  pause(): void
217
217
  resume(): void
package/types/header.d.ts CHANGED
@@ -5,6 +5,11 @@ import { Autocomplete } from './utility'
5
5
  */
6
6
  export type IncomingHttpHeaders = Record<string, string | string[] | undefined>
7
7
 
8
+ /**
9
+ * The header type declaration of `undici` for outgoing requests.
10
+ */
11
+ export type OutgoingHttpHeaders = Record<string, number | string | string[] | undefined>
12
+
8
13
  type HeaderNames = Autocomplete<
9
14
  | 'Accept'
10
15
  | 'Accept-CH'
@@ -8,7 +8,7 @@ export default Interceptors
8
8
  declare namespace Interceptors {
9
9
  export type DumpInterceptorOpts = { maxSize?: number }
10
10
  export type RetryInterceptorOpts = RetryHandler.RetryOptions
11
- export type RedirectInterceptorOpts = { maxRedirections?: number }
11
+ export type RedirectInterceptorOpts = { maxRedirections?: number, throwOnMaxRedirect?: boolean }
12
12
  export type DecompressInterceptorOpts = {
13
13
  skipErrorResponses?: boolean
14
14
  skipStatusCodes?: number[]
@@ -1,7 +1,7 @@
1
1
  import Agent from './agent'
2
2
  import buildConnector from './connector'
3
3
  import Dispatcher from './dispatcher'
4
- import { IncomingHttpHeaders } from './header'
4
+ import { OutgoingHttpHeaders } from './header'
5
5
 
6
6
  export default ProxyAgent
7
7
 
@@ -20,7 +20,7 @@ declare namespace ProxyAgent {
20
20
  */
21
21
  auth?: string;
22
22
  token?: string;
23
- headers?: IncomingHttpHeaders;
23
+ headers?: OutgoingHttpHeaders;
24
24
  requestTls?: buildConnector.BuildOptions;
25
25
  proxyTls?: buildConnector.BuildOptions;
26
26
  clientFactory?(origin: URL, opts: object): Dispatcher;
@@ -1,6 +1,6 @@
1
1
  import Dispatcher from './dispatcher'
2
2
  import buildConnector from './connector'
3
- import { IncomingHttpHeaders } from './header'
3
+ import { OutgoingHttpHeaders } from './header'
4
4
  import Pool from './pool'
5
5
 
6
6
  export default Socks5ProxyAgent
@@ -12,7 +12,7 @@ declare class Socks5ProxyAgent extends Dispatcher {
12
12
  declare namespace Socks5ProxyAgent {
13
13
  export interface Options extends Pool.Options {
14
14
  /** Additional headers to send with the proxy connection */
15
- headers?: IncomingHttpHeaders;
15
+ headers?: OutgoingHttpHeaders;
16
16
  /** SOCKS5 proxy username for authentication */
17
17
  username?: string;
18
18
  /** SOCKS5 proxy password for authentication */
File without changes