undici 8.9.0 → 8.10.1

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 (36) hide show
  1. package/docs/docs/api/Client.md +25 -8
  2. package/docs/docs/api/EnvHttpProxyAgent.md +3 -2
  3. package/docs/docs/api/EventSource.md +4 -0
  4. package/lib/api/readable.js +33 -43
  5. package/lib/cache/memory-cache-store.js +1 -1
  6. package/lib/core/connect.js +16 -2
  7. package/lib/core/symbols.js +1 -0
  8. package/lib/dispatcher/agent.js +7 -2
  9. package/lib/dispatcher/balanced-pool.js +1 -1
  10. package/lib/dispatcher/client-h1.js +9 -1
  11. package/lib/dispatcher/client-h2.js +221 -12
  12. package/lib/dispatcher/client.js +76 -30
  13. package/lib/dispatcher/dispatcher-base.js +13 -1
  14. package/lib/dispatcher/dispatcher.js +10 -0
  15. package/lib/dispatcher/env-http-proxy-agent.js +37 -8
  16. package/lib/dispatcher/proxy-agent.js +1 -1
  17. package/lib/dispatcher/round-robin-pool.js +1 -1
  18. package/lib/dispatcher/socks5-proxy-agent.js +18 -2
  19. package/lib/handler/cache-handler.js +8 -0
  20. package/lib/handler/decorator-handler.js +7 -1
  21. package/lib/handler/deduplication-handler.js +12 -6
  22. package/lib/handler/redirect-handler.js +8 -0
  23. package/lib/handler/retry-handler.js +90 -13
  24. package/lib/interceptor/cache.js +5 -2
  25. package/lib/interceptor/deduplicate.js +1 -1
  26. package/lib/interceptor/dump.js +1 -1
  27. package/lib/mock/mock-agent.js +13 -1
  28. package/lib/mock/mock-utils.js +217 -27
  29. package/lib/util/cache.js +2 -4
  30. package/lib/web/cookies/parse.js +4 -2
  31. package/lib/web/eventsource/eventsource-stream.js +28 -1
  32. package/lib/web/eventsource/eventsource.js +4 -1
  33. package/lib/web/fetch/request.js +2 -3
  34. package/lib/web/websocket/websocket.js +22 -0
  35. package/package.json +3 -3
  36. package/types/client.d.ts +50 -0
@@ -111,22 +111,35 @@ added: v1.0.0
111
111
  `autoSelectFamily` is enabled. **Default:** `250`.
112
112
  * `allowH2` {boolean} Enables HTTP/2 support when the server assigns it a
113
113
  higher priority through ALPN negotiation. **Default:** `true`.
114
- * `useH2c` {boolean} Enforces h2c (HTTP/2 cleartext) for non-HTTPS
115
- connections. **Default:** `false`.
116
- * `maxConcurrentStreams` {number} The maximum number of concurrent HTTP/2
114
+ * `useH2c` {boolean} _Deprecated: use h2Options.useH2c instead_ Enforces h2c (HTTP/2 cleartext) for non-HTTPS
115
+ connections. **Default:** `false`.
116
+ * `maxConcurrentStreams` {number} _Deprecated: use h2Options.useH2c instead_ The maximum number of concurrent HTTP/2
117
117
  streams for a single session. Once h2 is negotiated this — not `pipelining`,
118
118
  which is HTTP/1.1 only — is the ceiling used to dispatch in-flight requests.
119
119
  It may be overridden by the server's `SETTINGS_MAX_CONCURRENT_STREAMS`
120
120
  frame. **Default:** `100`.
121
- * `initialWindowSize` {number} The HTTP/2 stream-level flow-control window
122
- size (`SETTINGS_INITIAL_WINDOW_SIZE`). Must be a positive integer.
123
- **Default:** `262144`.
124
- * `connectionWindowSize` {number} The HTTP/2 connection-level flow-control
121
+ * `connectionWindowSize` {number} _Deprecated: use h2Options.connectionWindowSize instead_ The HTTP/2 connection-level flow-control
125
122
  window size set via `ClientHttp2Session.setLocalWindowSize()`. Must be a
126
123
  positive integer. **Default:** `524288`.
127
- * `pingInterval` {number} The time interval, in milliseconds, between HTTP/2
124
+ * `pingInterval` {number} _Deprecated: use h2Options.pingInterval instead_ The time interval, in milliseconds, between HTTP/2
128
125
  PING frames. Set to `0` to disable PING frames. Applies only to HTTP/2
129
126
  connections and emits a `ping` event on the client. **Default:** `60e3`.
127
+ * `h2Options` {object} Set of options for HTTP/2 sessions
128
+ * `useH2c` {boolean} Enforces h2c (HTTP/2 cleartext) for non-HTTPS
129
+ connections. **Default:** `false`.
130
+ * `maxConcurrentStreams` {number} The maximum number of concurrent HTTP/2
131
+ streams for a single session. Once h2 is negotiated this — not `pipelining`,
132
+ which is HTTP/1.1 only — is the ceiling used to dispatch in-flight requests.
133
+ It may be overridden by the server's `SETTINGS_MAX_CONCURRENT_STREAMS`
134
+ frame. **Default:** `100`.
135
+ * `connectionWindowSize` {number} The HTTP/2 connection-level flow-control
136
+ window size set via `ClientHttp2Session.setLocalWindowSize()`. Must be a
137
+ positive integer. **Default:** `524288`.
138
+ * `pingInterval` {number} The time interval, in milliseconds, between HTTP/2
139
+ PING frames. Set to `0` to disable PING frames. Applies only to HTTP/2
140
+ connections and emits a `ping` event on the client. **Default:** `60e3`.
141
+ * `settings` {object} `SETTINGS` frame options. For full reference, take a
142
+ look to [HTTP/2#Settings Object](https://nodejs.org/api/http2.html#settings-object)
130
143
  * `webSocket` {Object} (optional) WebSocket-specific configuration.
131
144
  * `maxFragments` {number} The maximum number of fragments in a message. Set
132
145
  to `0` to disable the limit. **Default:** `131072`.
@@ -134,6 +147,10 @@ added: v1.0.0
134
147
  WebSocket messages. Applied to uncompressed messages, compressed frame
135
148
  payloads, and decompressed (`permessage-deflate`) messages. Set to `0` to
136
149
  disable the limit. **Default:** `134217728`.
150
+ * `eventSource` {Object} (optional) EventSource-specific configuration.
151
+ * `maxEventSize` {number} The maximum allowed event size, in bytes, for
152
+ EventSource messages. Set to `0` to disable the limit.
153
+ **Default:** `buffer.kStringMaxLength`.
137
154
  * Returns: {Client}
138
155
 
139
156
  Instantiating a `Client` does not open a connection; the connection is
@@ -25,8 +25,9 @@ it is used only for HTTPS requests.
25
25
  proxied. Each entry may include a leading dot or `*.` wildcard (for example
26
26
  `.example.com`) to match subdomains, and an optional `:port` suffix to restrict
27
27
  the match to a specific port. A request bypasses the proxy when its host equals
28
- an entry or is a subdomain of one. Setting `no_proxy` to `*` bypasses the proxy
29
- for every request.
28
+ an entry or is a subdomain of one. A trailing dot is ignored on both sides, so
29
+ `example.com.` and `example.com` match each other. Setting `no_proxy` to `*`
30
+ bypasses the proxy for every request.
30
31
 
31
32
  The uppercase variants `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` are also
32
33
  honored. When both the lowercase and uppercase forms of a variable are set, the
@@ -66,6 +66,9 @@ added: v6.5.0
66
66
  wait before re-establishing a dropped connection. The server may override
67
67
  this value with a `retry` field. **Default:** `3000`.
68
68
 
69
+ EventSource-specific limits can be configured on the dispatcher using the
70
+ `eventSource` option. See [`Client`][] for details.
71
+
69
72
  Creates a new `EventSource` and immediately begins connecting to `url`. The
70
73
  request is sent with the `Accept: text/event-stream` header, a cache mode of
71
74
  `no-store`, and an initiator type of `other`.
@@ -349,6 +352,7 @@ eventSource.onerror = () => {
349
352
  ```
350
353
 
351
354
  [WHATWG-conformant]: https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
355
+ [`Client`]: Client.md#new-clienturl-options
352
356
  [`Dispatcher`]: Dispatcher.md#class-dispatcher
353
357
  [`addEventListener()`]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
354
358
  [server-sent events]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
@@ -15,7 +15,6 @@ const kContentType = Symbol('kContentType')
15
15
  const kContentLength = Symbol('kContentLength')
16
16
  const kUsed = Symbol('kUsed')
17
17
  const kBytesRead = Symbol('kBytesRead')
18
- const kPreservedBuffer = Symbol('kPreservedBuffer')
19
18
 
20
19
  const noop = () => {}
21
20
 
@@ -326,36 +325,14 @@ class BodyReadable extends Readable {
326
325
  */
327
326
  setEncoding (encoding) {
328
327
  if (Buffer.isEncoding(encoding)) {
329
- // Preserve raw Buffer chunks for the consume path (body.text(),
330
- // body.json(), etc.) before super.setEncoding() replaces them
331
- // with decoded strings. Without this, the consume path would
332
- // lose access to the original bytes — some of which may be held
333
- // by the decoder for incomplete multi-byte sequences, and the
334
- // rest converted to strings that can't be safely concatenated
335
- // byte-wise.
336
- const state = this._readableState
337
- const buffer = state.buffer
338
- if (buffer && state.length > 0) {
339
- const bufferIndex = state.bufferIndex ?? 0
340
- const preserved = []
341
- const source = typeof buffer.slice === 'function'
342
- ? buffer.slice(bufferIndex)
343
- : buffer
344
- for (const data of source) {
345
- if (Buffer.isBuffer(data)) {
346
- preserved.push(data)
347
- }
348
- }
349
- if (preserved.length > 0) {
350
- this[kPreservedBuffer] = (this[kPreservedBuffer] || []).concat(preserved)
351
- }
352
- }
353
-
354
328
  // Delegate to Node.js Readable.setEncoding() which initializes a
355
329
  // StringDecoder and re-encodes already-buffered chunks. This properly
356
330
  // handles multi-byte sequences split at chunk boundaries for the
357
331
  // for-await / on('data') paths. Without this, Node.js uses
358
332
  // buf.toString(encoding) on each chunk, producing U+FFFD for split chars.
333
+ //
334
+ // The consume path (body.text(), body.json(), ...) copes with the
335
+ // decoded strings this leaves in state.buffer, see consumeStart().
359
336
  super.setEncoding(encoding)
360
337
  }
361
338
  return this
@@ -464,17 +441,7 @@ function consumeStart (consume) {
464
441
 
465
442
  const { _readableState: state } = consume.stream
466
443
 
467
- // If setEncoding() was called, state.buffer may contain decoded strings
468
- // (which would break Buffer.concat in chunksDecode). Use the preserved
469
- // raw Buffers (saved before super.setEncoding() in setEncoding()) for
470
- // byte-level accurate consumption. Otherwise read from state.buffer.
471
- const preserved = consume.stream[kPreservedBuffer]
472
- if (preserved && preserved.length > 0) {
473
- for (const chunk of preserved) {
474
- consumePush(consume, chunk)
475
- }
476
- consume.stream[kPreservedBuffer] = null
477
- } else if (state.bufferIndex) {
444
+ if (state.bufferIndex) {
478
445
  const start = state.bufferIndex
479
446
  const end = state.buffer.length
480
447
  for (let n = start; n < end; n++) {
@@ -486,14 +453,29 @@ function consumeStart (consume) {
486
453
  }
487
454
  }
488
455
 
456
+ // If setEncoding() was called, state.buffer holds decoded strings, which
457
+ // consumePush() turns back into bytes. The trailing bytes of a multi-byte
458
+ // sequence split across a chunk boundary are not part of any of those
459
+ // strings, they are held inside the decoder until the rest arrives, so
460
+ // take them from there.
461
+ const decoder = state.decoder
462
+ if (decoder != null && decoder.lastNeed > 0) {
463
+ consumePush(consume, Buffer.from(decoder.lastChar.subarray(0, decoder.lastTotal - decoder.lastNeed)))
464
+ }
465
+
489
466
  if (state.endEmitted) {
490
- consumeEnd(this[kConsume], this._readableState.encoding)
491
- } else {
492
- consume.stream.on('end', function () {
493
- consumeEnd(this[kConsume], this._readableState.encoding)
494
- })
467
+ // No `this` to read the consume off here: consumeStart is a free function, called from
468
+ // the queueMicrotask above. The callback below does have one, because the emitter passes
469
+ // the stream as its receiver. Returning matters too - consumeEnd() clears consume.stream,
470
+ // which the resume() below would then dereference.
471
+ consumeEnd(consume, state.encoding)
472
+ return
495
473
  }
496
474
 
475
+ consume.stream.on('end', function () {
476
+ consumeEnd(this[kConsume], this._readableState.encoding)
477
+ })
478
+
497
479
  consume.stream.resume()
498
480
 
499
481
  while (consume.stream.read() != null) {
@@ -583,7 +565,7 @@ function consumeEnd (consume, encoding) {
583
565
 
584
566
  /**
585
567
  * @param {Consume} consume
586
- * @param {Buffer} chunk
568
+ * @param {Buffer|string} chunk
587
569
  * @returns {void}
588
570
  */
589
571
  function consumePush (consume, chunk) {
@@ -591,6 +573,14 @@ function consumePush (consume, chunk) {
591
573
  return
592
574
  }
593
575
 
576
+ if (typeof chunk === 'string') {
577
+ // Buffered before the consume started, while an encoding was set.
578
+ // consume.length has to stay a byte count and chunksDecode()/chunksConcat()
579
+ // only work on bytes, so re-encode. A string's own length is in UTF-16 code
580
+ // units and Uint8Array.prototype.set() ignores a string argument entirely.
581
+ chunk = Buffer.from(chunk, consume.stream._readableState.encoding)
582
+ }
583
+
594
584
  consume.length += chunk.length
595
585
  consume.body.push(chunk)
596
586
  }
@@ -179,7 +179,7 @@ class MemoryCacheStore extends EventEmitter {
179
179
 
180
180
  // Perform eviction
181
181
  for (const [key, entries] of store.#entries) {
182
- for (const entry of entries.splice(0, entries.length / 2)) {
182
+ for (const entry of entries.splice(0, Math.ceil(entries.length / 2))) {
183
183
  store.#size -= entry.size
184
184
  store.#count -= 1
185
185
  }
@@ -105,13 +105,27 @@ function buildConnector ({ allowH2, preferH2, useH2c, maxCachedSessions, socketP
105
105
 
106
106
  port = port || 80
107
107
 
108
- socket = net.connect({
108
+ const connectOptions = {
109
109
  highWaterMark: 64 * 1024, // Same as nodejs fs streams.
110
110
  ...options,
111
111
  localAddress,
112
112
  port,
113
113
  host: hostname
114
- })
114
+ }
115
+
116
+ const family = net.isIP(hostname)
117
+ if (family !== 0 && servername && servername !== hostname) {
118
+ connectOptions.host = servername
119
+ connectOptions.lookup = (_hostname, lookupOptions, cb) => {
120
+ if (lookupOptions.all) {
121
+ cb(null, [{ address: hostname, family }])
122
+ } else {
123
+ cb(null, hostname, family)
124
+ }
125
+ }
126
+ }
127
+
128
+ socket = net.connect(connectOptions)
115
129
  if (useH2c === true) {
116
130
  socket.alpnProtocol = 'h2'
117
131
  }
@@ -56,6 +56,7 @@ module.exports = {
56
56
  kCounter: Symbol('socket request counter'),
57
57
  kMaxResponseSize: Symbol('max response size'),
58
58
  kHTTP2Session: Symbol('http2Session'),
59
+ kHTTP2Options: Symbol('http2 options'),
59
60
  kHTTP2SessionState: Symbol('http2Session state'),
60
61
  kRetryHandlerDefaultRetry: Symbol('retry agent default retry'),
61
62
  kConstruct: Symbol('constructable'),
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { InvalidArgumentError, MaxOriginsReachedError } = require('../core/errors')
4
- const { kBusy, kClients, kConnected, kRunning, kClose, kDestroy, kDispatch, kUrl } = require('../core/symbols')
4
+ const { kBusy, kClients, kConnected, kRunning, kPending, kClose, kDestroy, kDispatch, kUrl } = require('../core/symbols')
5
5
  const DispatcherBase = require('./dispatcher-base')
6
6
  const Pool = require('./pool')
7
7
  const Client = require('./client')
@@ -97,7 +97,12 @@ class Agent extends DispatcherBase {
97
97
  return
98
98
  }
99
99
 
100
- if (dispatcher[kConnected] > 0 || dispatcher[kBusy]) {
100
+ // A GOAWAY detaches the HTTP/2 session before requeued requests are
101
+ // dispatched on a replacement connection. At that point the pool has
102
+ // no connected clients and is not busy, but it still has pending work.
103
+ // Closing it here lets the replacement Client finish those requests
104
+ // and then destroys that new connection with ClientDestroyedError.
105
+ if (dispatcher[kConnected] > 0 || dispatcher[kBusy] || dispatcher[kPending] > 0) {
101
106
  return
102
107
  }
103
108
 
@@ -54,7 +54,7 @@ class BalancedPool extends PoolBase {
54
54
  throw new InvalidArgumentError('factory must be a function.')
55
55
  }
56
56
 
57
- super()
57
+ super(opts)
58
58
 
59
59
  this[kOptions] = { ...util.deepClone(opts) }
60
60
  this[kIndex] = -1
@@ -1061,6 +1061,15 @@ function clearIdleSocketValidation (socket) {
1061
1061
 
1062
1062
  function scheduleIdleSocketValidation (client, socket) {
1063
1063
  socket[kIdleSocketValidation] = 1
1064
+ // Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
1065
+ // already pending on this idle keep-alive socket are processed before the
1066
+ // next request is written (GHSA-35p6-xmwp-9g52).
1067
+ //
1068
+ // setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
1069
+ // (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
1070
+ // block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
1071
+ // A ref'd Immediate both keeps the pending request alive and makes poll
1072
+ // return immediately — the hybrid those issues asked for.
1064
1073
  socket[kIdleSocketValidationTimeout] = setImmediate(() => {
1065
1074
  socket[kIdleSocketValidationTimeout] = null
1066
1075
  socket[kIdleSocketValidation] = 2
@@ -1069,7 +1078,6 @@ function scheduleIdleSocketValidation (client, socket) {
1069
1078
  client[kResume]()
1070
1079
  }
1071
1080
  })
1072
- socket[kIdleSocketValidationTimeout].unref?.()
1073
1081
  }
1074
1082
 
1075
1083
  /**