undici 6.26.0 → 6.28.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.
@@ -27,6 +27,7 @@ Returns: `Client`
27
27
  * **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB.
28
28
  * **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
29
29
  * **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options.
30
+ * **maxFragments** `number` (optional) - Default: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit.
30
31
  * **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit.
31
32
  * **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
32
33
  * **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
@@ -350,7 +350,13 @@ function processHeader (request, key, val) {
350
350
  } else if (typeof val[i] === 'object') {
351
351
  throw new InvalidArgumentError(`invalid ${key} header`)
352
352
  } else {
353
- arr.push(`${val[i]}`)
353
+ // Coerce primitives (and reject unsafe coercions such as functions
354
+ // with a crafted toString/Symbol.toPrimitive).
355
+ const str = `${val[i]}`
356
+ if (!isValidHeaderValue(str)) {
357
+ throw new InvalidArgumentError(`invalid ${key} header`)
358
+ }
359
+ arr.push(str)
354
360
  }
355
361
  }
356
362
  val = arr
@@ -361,7 +367,12 @@ function processHeader (request, key, val) {
361
367
  } else if (val === null) {
362
368
  val = ''
363
369
  } else {
370
+ // Coerce primitives (and reject unsafe coercions such as functions
371
+ // with a crafted toString/Symbol.toPrimitive).
364
372
  val = `${val}`
373
+ if (!isValidHeaderValue(val)) {
374
+ throw new InvalidArgumentError(`invalid ${key} header`)
375
+ }
365
376
  }
366
377
 
367
378
  if (headerName === 'host') {
@@ -10,6 +10,7 @@ const {
10
10
  RequestContentLengthMismatchError,
11
11
  ResponseContentLengthMismatchError,
12
12
  RequestAbortedError,
13
+ InvalidArgumentError,
13
14
  HeadersTimeoutError,
14
15
  HeadersOverflowError,
15
16
  SocketError,
@@ -57,6 +58,9 @@ const EMPTY_BUF = Buffer.alloc(0)
57
58
  const FastBuffer = Buffer[Symbol.species]
58
59
  const addListener = util.addListener
59
60
  const removeAllListeners = util.removeAllListeners
61
+ const kIdleSocketValidation = Symbol('kIdleSocketValidation')
62
+ const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout')
63
+ const kSocketUsed = Symbol('kSocketUsed')
60
64
 
61
65
  let extractBody
62
66
 
@@ -371,6 +375,11 @@ class Parser {
371
375
  return -1
372
376
  }
373
377
 
378
+ if (client[kRunning] === 0) {
379
+ util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
380
+ return -1
381
+ }
382
+
374
383
  const request = client[kQueue][client[kRunningIdx]]
375
384
  if (!request) {
376
385
  return -1
@@ -474,6 +483,11 @@ class Parser {
474
483
  return -1
475
484
  }
476
485
 
486
+ if (client[kRunning] === 0) {
487
+ util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
488
+ return -1
489
+ }
490
+
477
491
  const request = client[kQueue][client[kRunningIdx]]
478
492
 
479
493
  /* istanbul ignore next: difficult to make a test case for */
@@ -647,6 +661,7 @@ class Parser {
647
661
  request.onComplete(headers)
648
662
 
649
663
  client[kQueue][client[kRunningIdx]++] = null
664
+ socket[kSocketUsed] = true
650
665
 
651
666
  if (socket[kWriting]) {
652
667
  assert(client[kRunning] === 0)
@@ -705,6 +720,9 @@ async function connectH1 (client, socket) {
705
720
  socket[kWriting] = false
706
721
  socket[kReset] = false
707
722
  socket[kBlocking] = false
723
+ socket[kIdleSocketValidation] = 0
724
+ socket[kIdleSocketValidationTimeout] = null
725
+ socket[kSocketUsed] = false
708
726
  socket[kParser] = new Parser(client, socket, llhttpInstance)
709
727
 
710
728
  addListener(socket, 'error', function (err) {
@@ -751,6 +769,8 @@ async function connectH1 (client, socket) {
751
769
  const client = this[kClient]
752
770
  const parser = this[kParser]
753
771
 
772
+ clearIdleSocketValidation(this)
773
+
754
774
  if (parser) {
755
775
  if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
756
776
  this[kError] = parser.finish() || this[kError]
@@ -816,7 +836,7 @@ async function connectH1 (client, socket) {
816
836
  return socket.destroyed
817
837
  },
818
838
  busy (request) {
819
- if (socket[kWriting] || socket[kReset] || socket[kBlocking]) {
839
+ if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) {
820
840
  return true
821
841
  }
822
842
 
@@ -854,6 +874,31 @@ async function connectH1 (client, socket) {
854
874
  }
855
875
  }
856
876
 
877
+ function clearIdleSocketValidation (socket) {
878
+ if (socket[kIdleSocketValidationTimeout]) {
879
+ clearTimeout(socket[kIdleSocketValidationTimeout])
880
+ socket[kIdleSocketValidationTimeout] = null
881
+ }
882
+
883
+ socket[kIdleSocketValidation] = 0
884
+ }
885
+
886
+ function scheduleIdleSocketValidation (client, socket) {
887
+ socket[kIdleSocketValidation] = 1
888
+ socket[kIdleSocketValidationTimeout] = setTimeout(() => {
889
+ socket[kIdleSocketValidationTimeout] = null
890
+ socket[kIdleSocketValidation] = 2
891
+
892
+ if (client[kSocket] === socket && !socket.destroyed) {
893
+ client[kResume]()
894
+ }
895
+ }, 0)
896
+ socket[kIdleSocketValidationTimeout].unref?.()
897
+ }
898
+
899
+ /**
900
+ * @param {import('./client.js')} client
901
+ */
857
902
  function resumeH1 (client) {
858
903
  const socket = client[kSocket]
859
904
 
@@ -868,6 +913,32 @@ function resumeH1 (client) {
868
913
  socket[kNoRef] = false
869
914
  }
870
915
 
916
+ if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) {
917
+ if (socket[kIdleSocketValidation] === 0) {
918
+ scheduleIdleSocketValidation(client, socket)
919
+ socket[kParser].readMore()
920
+ if (socket.destroyed) {
921
+ return
922
+ }
923
+ return
924
+ }
925
+
926
+ if (socket[kIdleSocketValidation] === 1) {
927
+ socket[kParser].readMore()
928
+ if (socket.destroyed) {
929
+ return
930
+ }
931
+ return
932
+ }
933
+ }
934
+
935
+ if (client[kRunning] === 0) {
936
+ socket[kParser].readMore()
937
+ if (socket.destroyed) {
938
+ return
939
+ }
940
+ }
941
+
871
942
  if (client[kSize] === 0) {
872
943
  if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
873
944
  socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE)
@@ -923,8 +994,16 @@ function writeH1 (client, request) {
923
994
  }
924
995
  body = bodyStream.stream
925
996
  contentLength = bodyStream.length
926
- } else if (util.isBlobLike(body) && request.contentType == null && body.type) {
927
- headers.push('content-type', body.type)
997
+ } else if (util.isBlobLike(body) && request.contentType == null) {
998
+ const contentType = body.type
999
+ if (contentType) {
1000
+ const contentTypeValue = `${contentType}`
1001
+ if (!util.isValidHeaderValue(contentTypeValue)) {
1002
+ util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header'))
1003
+ return false
1004
+ }
1005
+ headers.push('content-type', contentTypeValue)
1006
+ }
928
1007
  }
929
1008
 
930
1009
  if (body && typeof body.read === 'function') {
@@ -961,6 +1040,7 @@ function writeH1 (client, request) {
961
1040
  }
962
1041
 
963
1042
  const socket = client[kSocket]
1043
+ clearIdleSocketValidation(socket)
964
1044
 
965
1045
  const abort = (err) => {
966
1046
  if (request.aborted || request.completed) {
@@ -26,6 +26,7 @@ class DispatcherBase extends Dispatcher {
26
26
 
27
27
  get webSocketOptions () {
28
28
  return {
29
+ maxFragments: this[kWebSocketOptions].maxFragments ?? 131072,
29
30
  maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024
30
31
  }
31
32
  }
@@ -15,6 +15,28 @@ function calculateRetryAfterHeader (retryAfter) {
15
15
  return new Date(retryAfter).getTime() - current
16
16
  }
17
17
 
18
+ function validatePartialResponseContentLength (headers, range, statusCode, retryCount) {
19
+ const contentLength = headers['content-length']
20
+ if (contentLength == null) {
21
+ return null
22
+ }
23
+
24
+ if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) {
25
+ return null
26
+ }
27
+
28
+ const length = Number(contentLength)
29
+ const expectedLength = range.end - range.start + 1
30
+ if (!Number.isFinite(length) || length !== expectedLength) {
31
+ return new RequestRetryError('Content-Length mismatch', statusCode, {
32
+ headers,
33
+ data: { count: retryCount }
34
+ })
35
+ }
36
+
37
+ return null
38
+ }
39
+
18
40
  class RetryHandler {
19
41
  constructor (opts, handlers) {
20
42
  const { retryOptions, ...dispatchOpts } = opts
@@ -229,6 +251,12 @@ class RetryHandler {
229
251
  return false
230
252
  }
231
253
 
254
+ const contentLengthError = validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount)
255
+ if (contentLengthError != null) {
256
+ this.abort(contentLengthError)
257
+ return false
258
+ }
259
+
232
260
  const { start, size, end = size - 1 } = contentRange
233
261
 
234
262
  assert(this.start === start, 'content-range mismatch')
@@ -252,6 +280,12 @@ class RetryHandler {
252
280
  )
253
281
  }
254
282
 
283
+ const contentLengthError = validatePartialResponseContentLength(headers, range, statusCode, this.retryCount)
284
+ if (contentLengthError != null) {
285
+ this.abort(contentLengthError)
286
+ return false
287
+ }
288
+
255
289
  const { start, size, end = size - 1 } = range
256
290
  assert(
257
291
  start != null && Number.isFinite(start),
@@ -275,32 +275,25 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
275
275
  // If the attribute-name case-insensitively matches the string
276
276
  // "SameSite", the user agent MUST process the cookie-av as follows:
277
277
 
278
- // 1. Let enforcement be "Default".
279
- let enforcement = 'Default'
280
-
281
278
  const attributeValueLowercase = attributeValue.toLowerCase()
282
- // 2. If cookie-av's attribute-value is a case-insensitive match for
283
- // "None", set enforcement to "None".
284
- if (attributeValueLowercase.includes('none')) {
285
- enforcement = 'None'
286
- }
287
279
 
288
- // 3. If cookie-av's attribute-value is a case-insensitive match for
289
- // "Strict", set enforcement to "Strict".
290
- if (attributeValueLowercase.includes('strict')) {
291
- enforcement = 'Strict'
280
+ // 1. If cookie-av's attribute-value is a case-insensitive match for
281
+ // "None", append an attribute to the cookie-attribute-list with an
282
+ // attribute-name of "SameSite" and an attribute-value of "None".
283
+ if (attributeValueLowercase === 'none') {
284
+ cookieAttributeList.sameSite = 'None'
285
+ } else if (attributeValueLowercase === 'strict') {
286
+ // 2. If cookie-av's attribute-value is a case-insensitive match for
287
+ // "Strict", append an attribute to the cookie-attribute-list with
288
+ // an attribute-name of "SameSite" and an attribute-value of
289
+ // "Strict".
290
+ cookieAttributeList.sameSite = 'Strict'
291
+ } else if (attributeValueLowercase === 'lax') {
292
+ // 3. If cookie-av's attribute-value is a case-insensitive match for
293
+ // "Lax", append an attribute to the cookie-attribute-list with an
294
+ // attribute-name of "SameSite" and an attribute-value of "Lax".
295
+ cookieAttributeList.sameSite = 'Lax'
292
296
  }
293
-
294
- // 4. If cookie-av's attribute-value is a case-insensitive match for
295
- // "Lax", set enforcement to "Lax".
296
- if (attributeValueLowercase.includes('lax')) {
297
- enforcement = 'Lax'
298
- }
299
-
300
- // 5. Append an attribute to the cookie-attribute-list with an
301
- // attribute-name of "SameSite" and an attribute-value of
302
- // enforcement.
303
- cookieAttributeList.sameSite = enforcement
304
297
  } else {
305
298
  cookieAttributeList.unparsed ??= []
306
299
 
@@ -105,7 +105,7 @@ function validateCookiePath (path) {
105
105
 
106
106
  if (
107
107
  code < 0x20 || // exclude CTLs (0-31)
108
- code === 0x7F || // DEL
108
+ code > 0x7E || // exclude DEL and non-ascii
109
109
  code === 0x3B // ;
110
110
  ) {
111
111
  throw new Error('Invalid cookie path')
@@ -114,16 +114,80 @@ function validateCookiePath (path) {
114
114
  }
115
115
 
116
116
  /**
117
- * I have no idea why these values aren't allowed to be honest,
118
- * but Deno tests these. - Khafra
117
+ * <let-dig> ::= <letter> | <digit>
118
+ *
119
+ * <letter> ::= any one of the 52 alphabetic characters A through Z in
120
+ * upper case and a through z in lower case
121
+ *
122
+ * <digit> ::= any one of the ten digits 0 through 9r
123
+ *
124
+ * @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
125
+ * @param {number} code
126
+ */
127
+ function isLetterOrDigit (code) {
128
+ return (
129
+ (code >= 0x30 && code <= 0x39) || // 0-9
130
+ (code >= 0x41 && code <= 0x5A) || // A-Z
131
+ (code >= 0x61 && code <= 0x7A) // a-z
132
+ )
133
+ }
134
+
135
+ /**
136
+ * Validates a cookie domain against the "preferred name syntax".
137
+ *
138
+ * <domain> ::= <subdomain> | " "
139
+ * <subdomain> ::= <label> | <subdomain> "." <label>
140
+ * <label> ::= <let-dig> [ [ <ldh-str> ] <let-dig> ]
141
+ * <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
142
+ * <let-dig-hyp> ::= <let-dig> | "-"
143
+ *
144
+ * @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
145
+ * @see https://www.rfc-editor.org/rfc/rfc1123#section-2.1
146
+ * @see https://www.rfc-editor.org/rfc/rfc1035#section-2.3.4
119
147
  * @param {string} domain
120
148
  */
121
149
  function validateCookieDomain (domain) {
122
- if (
123
- domain.startsWith('-') ||
124
- domain.endsWith('.') ||
125
- domain.endsWith('-')
126
- ) {
150
+ // <domain> ::= <subdomain> | " "
151
+ if (domain === ' ') {
152
+ return
153
+ }
154
+
155
+ if (domain.length > 255) {
156
+ throw new Error('Invalid cookie domain')
157
+ }
158
+
159
+ let labelLength = 0
160
+
161
+ for (let i = 0; i < domain.length; ++i) {
162
+ const code = domain.charCodeAt(i)
163
+
164
+ if (code === 0x2E) {
165
+ if (labelLength === 0) {
166
+ throw new Error('Invalid cookie domain')
167
+ }
168
+
169
+ if (domain.charCodeAt(i - 1) === 0x2D) { // "-"
170
+ throw new Error('Invalid cookie domain')
171
+ }
172
+
173
+ labelLength = 0
174
+ continue
175
+ }
176
+
177
+ if (labelLength === 0 && !isLetterOrDigit(code)) {
178
+ throw new Error('Invalid cookie domain')
179
+ }
180
+
181
+ if (!isLetterOrDigit(code) && code !== 0x2D) { // "-"
182
+ throw new Error('Invalid cookie domain')
183
+ }
184
+
185
+ if (++labelLength > 63) {
186
+ throw new Error('Invalid cookie domain')
187
+ }
188
+ }
189
+
190
+ if (labelLength === 0 || domain.charCodeAt(domain.length - 1) === 0x2D) { // "-"
127
191
  throw new Error('Invalid cookie domain')
128
192
  }
129
193
  }
@@ -266,7 +330,13 @@ function stringify (cookie) {
266
330
 
267
331
  const [key, ...value] = part.split('=')
268
332
 
269
- out.push(`${key.trim()}=${value.join('=')}`)
333
+ const trimmedKey = key.trim()
334
+ const joinedValue = value.join('=')
335
+
336
+ validateCookieName(trimmedKey)
337
+ validateCookieValue(joinedValue)
338
+
339
+ out.push(`${trimmedKey}=${joinedValue}`)
270
340
  }
271
341
 
272
342
  return out.join('; ')
@@ -20,6 +20,11 @@ const { closeWebSocketConnection } = require('./connection')
20
20
  const { PerMessageDeflate } = require('./permessage-deflate')
21
21
  const { MessageSizeExceededError } = require('../../core/errors')
22
22
 
23
+ function failWebsocketConnectionWithCode (ws, code, reason) {
24
+ closeWebSocketConnection(ws, code, reason, Buffer.byteLength(reason))
25
+ failWebsocketConnection(ws, reason)
26
+ }
27
+
23
28
  // This code was influenced by ws released under the MIT license.
24
29
  // Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
25
30
  // Copyright (c) 2013 Arnout Kazemier and contributors
@@ -39,19 +44,23 @@ class ByteParser extends Writable {
39
44
  /** @type {Map<string, PerMessageDeflate>} */
40
45
  #extensions
41
46
 
47
+ /** @type {number} */
48
+ #maxFragments
49
+
42
50
  /** @type {number} */
43
51
  #maxPayloadSize
44
52
 
45
53
  /**
46
54
  * @param {import('./websocket').WebSocket} ws
47
55
  * @param {Map<string, string>|null} extensions
48
- * @param {{ maxPayloadSize?: number }} [options]
56
+ * @param {{ maxFragments?: number, maxPayloadSize?: number }} [options]
49
57
  */
50
58
  constructor (ws, extensions, options = {}) {
51
59
  super()
52
60
 
53
61
  this.ws = ws
54
62
  this.#extensions = extensions == null ? new Map() : extensions
63
+ this.#maxFragments = options.maxFragments ?? 0
55
64
  this.#maxPayloadSize = options.maxPayloadSize ?? 0
56
65
 
57
66
  if (this.#extensions.has('permessage-deflate')) {
@@ -75,9 +84,9 @@ class ByteParser extends Writable {
75
84
  if (
76
85
  this.#maxPayloadSize > 0 &&
77
86
  !isControlFrame(this.#info.opcode) &&
78
- this.#info.payloadLength > this.#maxPayloadSize
87
+ this.#info.payloadLength + this.#fragmentsBytes > this.#maxPayloadSize
79
88
  ) {
80
- failWebsocketConnection(this.ws, 'Payload size exceeds maximum allowed size')
89
+ failWebsocketConnectionWithCode(this.ws, 1009, 'Payload size exceeds maximum allowed size')
81
90
  return false
82
91
  }
83
92
 
@@ -242,10 +251,12 @@ class ByteParser extends Writable {
242
251
  this.#state = parserStates.INFO
243
252
  } else {
244
253
  if (!this.#info.compressed) {
245
- this.writeFragments(body)
254
+ if (!this.writeFragments(body)) {
255
+ return
256
+ }
246
257
 
247
258
  if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
248
- failWebsocketConnection(this.ws, new MessageSizeExceededError().message)
259
+ failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message)
249
260
  return
250
261
  }
251
262
 
@@ -264,14 +275,17 @@ class ByteParser extends Writable {
264
275
  this.#info.fin,
265
276
  (error, data) => {
266
277
  if (error) {
267
- failWebsocketConnection(this.ws, error.message)
278
+ const code = error instanceof MessageSizeExceededError ? 1009 : 1007
279
+ failWebsocketConnectionWithCode(this.ws, code, error.message)
268
280
  return
269
281
  }
270
282
 
271
- this.writeFragments(data)
283
+ if (!this.writeFragments(data)) {
284
+ return
285
+ }
272
286
 
273
287
  if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) {
274
- failWebsocketConnection(this.ws, new MessageSizeExceededError().message)
288
+ failWebsocketConnectionWithCode(this.ws, 1009, new MessageSizeExceededError().message)
275
289
  return
276
290
  }
277
291
 
@@ -341,8 +355,17 @@ class ByteParser extends Writable {
341
355
  }
342
356
 
343
357
  writeFragments (fragment) {
358
+ if (
359
+ this.#maxFragments > 0 &&
360
+ this.#fragments.length === this.#maxFragments
361
+ ) {
362
+ failWebsocketConnectionWithCode(this.ws, 1008, 'Too many message fragments')
363
+ return false
364
+ }
365
+
344
366
  this.#fragmentsBytes += fragment.length
345
367
  this.#fragments.push(fragment)
368
+ return true
346
369
  }
347
370
 
348
371
  consumeFragments () {
@@ -435,9 +435,12 @@ class WebSocket extends EventTarget {
435
435
  // once this happens, the connection is open
436
436
  this[kResponse] = response
437
437
 
438
- const maxPayloadSize = this[kController]?.dispatcher?.webSocketOptions?.maxPayloadSize
438
+ const webSocketOptions = this[kController]?.dispatcher?.webSocketOptions
439
+ const maxFragments = webSocketOptions?.maxFragments
440
+ const maxPayloadSize = webSocketOptions?.maxPayloadSize
439
441
 
440
442
  const parser = new ByteParser(this, parsedExtensions, {
443
+ maxFragments,
441
444
  maxPayloadSize
442
445
  })
443
446
  parser.on('drain', onParserDrain)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "6.26.0",
3
+ "version": "6.28.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
@@ -106,6 +106,12 @@ export declare namespace Client {
106
106
  bytesRead?: number
107
107
  }
108
108
  export interface WebSocketOptions {
109
+ /**
110
+ * Maximum number of fragments in a message.
111
+ * Set to 0 to disable the limit.
112
+ * @default 131072
113
+ */
114
+ maxFragments?: number;
109
115
  /**
110
116
  * Maximum allowed payload size in bytes for WebSocket messages.
111
117
  * Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages.