undici 6.26.0 → 6.27.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`.
@@ -57,6 +57,9 @@ const EMPTY_BUF = Buffer.alloc(0)
57
57
  const FastBuffer = Buffer[Symbol.species]
58
58
  const addListener = util.addListener
59
59
  const removeAllListeners = util.removeAllListeners
60
+ const kIdleSocketValidation = Symbol('kIdleSocketValidation')
61
+ const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout')
62
+ const kSocketUsed = Symbol('kSocketUsed')
60
63
 
61
64
  let extractBody
62
65
 
@@ -371,6 +374,11 @@ class Parser {
371
374
  return -1
372
375
  }
373
376
 
377
+ if (client[kRunning] === 0) {
378
+ util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
379
+ return -1
380
+ }
381
+
374
382
  const request = client[kQueue][client[kRunningIdx]]
375
383
  if (!request) {
376
384
  return -1
@@ -474,6 +482,11 @@ class Parser {
474
482
  return -1
475
483
  }
476
484
 
485
+ if (client[kRunning] === 0) {
486
+ util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket)))
487
+ return -1
488
+ }
489
+
477
490
  const request = client[kQueue][client[kRunningIdx]]
478
491
 
479
492
  /* istanbul ignore next: difficult to make a test case for */
@@ -647,6 +660,7 @@ class Parser {
647
660
  request.onComplete(headers)
648
661
 
649
662
  client[kQueue][client[kRunningIdx]++] = null
663
+ socket[kSocketUsed] = true
650
664
 
651
665
  if (socket[kWriting]) {
652
666
  assert(client[kRunning] === 0)
@@ -705,6 +719,9 @@ async function connectH1 (client, socket) {
705
719
  socket[kWriting] = false
706
720
  socket[kReset] = false
707
721
  socket[kBlocking] = false
722
+ socket[kIdleSocketValidation] = 0
723
+ socket[kIdleSocketValidationTimeout] = null
724
+ socket[kSocketUsed] = false
708
725
  socket[kParser] = new Parser(client, socket, llhttpInstance)
709
726
 
710
727
  addListener(socket, 'error', function (err) {
@@ -751,6 +768,8 @@ async function connectH1 (client, socket) {
751
768
  const client = this[kClient]
752
769
  const parser = this[kParser]
753
770
 
771
+ clearIdleSocketValidation(this)
772
+
754
773
  if (parser) {
755
774
  if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
756
775
  this[kError] = parser.finish() || this[kError]
@@ -816,7 +835,7 @@ async function connectH1 (client, socket) {
816
835
  return socket.destroyed
817
836
  },
818
837
  busy (request) {
819
- if (socket[kWriting] || socket[kReset] || socket[kBlocking]) {
838
+ if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) {
820
839
  return true
821
840
  }
822
841
 
@@ -854,6 +873,31 @@ async function connectH1 (client, socket) {
854
873
  }
855
874
  }
856
875
 
876
+ function clearIdleSocketValidation (socket) {
877
+ if (socket[kIdleSocketValidationTimeout]) {
878
+ clearTimeout(socket[kIdleSocketValidationTimeout])
879
+ socket[kIdleSocketValidationTimeout] = null
880
+ }
881
+
882
+ socket[kIdleSocketValidation] = 0
883
+ }
884
+
885
+ function scheduleIdleSocketValidation (client, socket) {
886
+ socket[kIdleSocketValidation] = 1
887
+ socket[kIdleSocketValidationTimeout] = setTimeout(() => {
888
+ socket[kIdleSocketValidationTimeout] = null
889
+ socket[kIdleSocketValidation] = 2
890
+
891
+ if (client[kSocket] === socket && !socket.destroyed) {
892
+ client[kResume]()
893
+ }
894
+ }, 0)
895
+ socket[kIdleSocketValidationTimeout].unref?.()
896
+ }
897
+
898
+ /**
899
+ * @param {import('./client.js')} client
900
+ */
857
901
  function resumeH1 (client) {
858
902
  const socket = client[kSocket]
859
903
 
@@ -868,6 +912,32 @@ function resumeH1 (client) {
868
912
  socket[kNoRef] = false
869
913
  }
870
914
 
915
+ if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) {
916
+ if (socket[kIdleSocketValidation] === 0) {
917
+ scheduleIdleSocketValidation(client, socket)
918
+ socket[kParser].readMore()
919
+ if (socket.destroyed) {
920
+ return
921
+ }
922
+ return
923
+ }
924
+
925
+ if (socket[kIdleSocketValidation] === 1) {
926
+ socket[kParser].readMore()
927
+ if (socket.destroyed) {
928
+ return
929
+ }
930
+ return
931
+ }
932
+ }
933
+
934
+ if (client[kRunning] === 0) {
935
+ socket[kParser].readMore()
936
+ if (socket.destroyed) {
937
+ return
938
+ }
939
+ }
940
+
871
941
  if (client[kSize] === 0) {
872
942
  if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
873
943
  socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE)
@@ -961,6 +1031,7 @@ function writeH1 (client, request) {
961
1031
  }
962
1032
 
963
1033
  const socket = client[kSocket]
1034
+ clearIdleSocketValidation(socket)
964
1035
 
965
1036
  const abort = (err) => {
966
1037
  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
  }
@@ -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
 
@@ -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.27.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.