@depup/undici 8.10.2-depup.0 → 8.11.2-depup.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 (50) hide show
  1. package/README.md +2 -2
  2. package/changes.json +1 -1
  3. package/docs/docs/api/Agent.md +8 -8
  4. package/docs/docs/api/DiagnosticsChannel.md +9 -6
  5. package/docs/docs/api/Dispatcher.md +9 -0
  6. package/docs/docs/api/Errors.md +25 -0
  7. package/docs/docs/api/Fetch.md +10 -0
  8. package/docs/docs/api/Interceptors.md +4 -3
  9. package/docs/docs/api/Pool.md +5 -6
  10. package/docs/docs/index.md +10 -1
  11. package/lib/api/readable.js +1 -2
  12. package/lib/core/request.js +23 -2
  13. package/lib/core/util.js +0 -39
  14. package/lib/dispatcher/client-h1.js +11 -4
  15. package/lib/dispatcher/client-h2.js +52 -8
  16. package/lib/dispatcher/client.js +6 -1
  17. package/lib/dispatcher/env-http-proxy-agent.js +21 -6
  18. package/lib/dispatcher/pool-base.js +63 -2
  19. package/lib/dispatcher/pool.js +90 -7
  20. package/lib/dispatcher/proxy-agent.js +16 -3
  21. package/lib/handler/deduplication-handler.js +24 -11
  22. package/lib/handler/redirect-handler.js +5 -0
  23. package/lib/handler/retry-handler.js +72 -13
  24. package/lib/interceptor/cache.js +4 -1
  25. package/lib/interceptor/decompress.js +236 -69
  26. package/lib/web/cache/cache.js +17 -8
  27. package/lib/web/cache/cachestorage.js +11 -5
  28. package/lib/web/cookies/index.js +19 -7
  29. package/lib/web/cookies/util.js +3 -1
  30. package/lib/web/eventsource/eventsource.js +25 -1
  31. package/lib/web/fetch/body.js +22 -15
  32. package/lib/web/fetch/data-url.js +6 -6
  33. package/lib/web/fetch/formdata.js +30 -36
  34. package/lib/web/fetch/headers.js +34 -31
  35. package/lib/web/fetch/index.js +46 -25
  36. package/lib/web/fetch/request.js +74 -79
  37. package/lib/web/fetch/response.js +43 -44
  38. package/lib/web/fetch/util.js +7 -7
  39. package/lib/web/infra/index.js +16 -1
  40. package/lib/web/webidl/index.js +2 -14
  41. package/lib/web/websocket/connection.js +33 -12
  42. package/lib/web/websocket/events.js +43 -26
  43. package/lib/web/websocket/receiver.js +2 -0
  44. package/lib/web/websocket/stream/websocketerror.js +13 -10
  45. package/lib/web/websocket/stream/websocketstream.js +19 -4
  46. package/lib/web/websocket/websocket.js +44 -41
  47. package/package.json +3 -3
  48. package/types/diagnostics-channel.d.ts +2 -1
  49. package/types/interceptors.d.ts +1 -1
  50. package/types/webidl.d.ts +13 -7
package/README.md CHANGED
@@ -13,8 +13,8 @@ npm install @depup/undici
13
13
 
14
14
  | Field | Value |
15
15
  |-------|-------|
16
- | Original | [undici](https://www.npmjs.com/package/undici) @ 8.10.2 |
17
- | Processed | 2026-09-06 |
16
+ | Original | [undici](https://www.npmjs.com/package/undici) @ 8.11.2 |
17
+ | Processed | 2026-09-27 |
18
18
  | Smoke test | passed |
19
19
  | Deps updated | 0 |
20
20
 
package/changes.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "bumped": {},
3
- "timestamp": "2026-09-06T01:00:35.415Z",
3
+ "timestamp": "2026-09-27T01:04:31.652Z",
4
4
  "totalUpdated": 0
5
5
  }
@@ -56,16 +56,16 @@ changes:
56
56
  `Infinity`, no limit is enforced. Must be a number greater than `0`.
57
57
  **Default:** `Infinity`.
58
58
 
59
- `Agent` inherits all {PoolOptions} (and therefore all {ClientOptions}). The
60
- per-origin {Pool} it creates uses the default unlimited `connections`, so
61
- concurrent requests to the same origin are spread across separate {Client}
62
- instances on separate sockets.
59
+ `Agent` inherits all {PoolOptions} (and therefore all {ClientOptions}). Each
60
+ origin gets a separate {Pool}, with `connections` acting as the maximum number
61
+ of clients that pool may create.
63
62
 
64
63
  > [!NOTE]
65
- > Because each concurrent request to an origin may use a different {Client},
66
- > HTTP/2 multiplexing on a shared session does not apply unless `connections` is
67
- > set to a small value (for example `connections: 1`). See {PoolOptions} and
68
- > {ClientOptions} for the full set of inherited options such as `allowH2`
64
+ > For an h2-capable HTTPS origin, the per-origin pool waits for the first TLS
65
+ > connection to finish ALPN negotiation. If the server selects h2, concurrent
66
+ > requests share that session up to `maxConcurrentStreams`. If it selects
67
+ > HTTP/1.1, normal connection fan-out resumes up to `connections`. See
68
+ > {PoolOptions} and {ClientOptions} for inherited options such as `allowH2`
69
69
  > (default `true`) and `maxConcurrentStreams` (default `100`).
70
70
 
71
71
  ### `agent.closed`
@@ -120,7 +120,8 @@ diagnosticsChannel.channel('undici:request:bodySent').subscribe(({ request }) =>
120
120
  added: v6.3.0
121
121
  -->
122
122
 
123
- Published after the response headers have been received.
123
+ Published after the response headers have been received. This includes a
124
+ successful CONNECT or protocol upgrade response.
124
125
 
125
126
  * `message` {Object}
126
127
  * `request` {Object} The same object published by
@@ -128,8 +129,9 @@ Published after the response headers have been received.
128
129
  * `response` {Object} The response being received.
129
130
  * `statusCode` {number} The HTTP status code.
130
131
  * `statusText` {string} The HTTP status message.
131
- * `headers` {Buffer[]} The raw response headers as an array of buffers,
132
- alternating between header name and value.
132
+ * `headers` {Buffer[]|Object} HTTP/1.1 response headers are an array of
133
+ buffers alternating between header name and value. HTTP/2 response
134
+ headers are an object.
133
135
 
134
136
  ```mjs
135
137
  import diagnosticsChannel from 'node:diagnostics_channel'
@@ -137,7 +139,7 @@ import diagnosticsChannel from 'node:diagnostics_channel'
137
139
  diagnosticsChannel.channel('undici:request:headers').subscribe(({ request, response }) => {
138
140
  console.log('statusCode', response.statusCode)
139
141
  console.log(response.statusText)
140
- console.log(response.headers.map((x) => x.toString()))
142
+ console.log(response.headers)
141
143
  })
142
144
  ```
143
145
 
@@ -168,8 +170,9 @@ diagnosticsChannel.channel('undici:request:bodyChunkReceived').subscribe(({ requ
168
170
  added: v6.3.0
169
171
  -->
170
172
 
171
- Published after the response body and trailers have been received, that is, once
172
- the response has fully completed.
173
+ Published once the response has fully completed. After an upgraded socket has
174
+ been passed to the request handler, this event is published with an empty
175
+ `trailers` array.
173
176
 
174
177
  * `message` {Object}
175
178
  * `request` {Object} The same object published by
@@ -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
@@ -446,6 +446,31 @@ The response returned an error status code. This is raised, for example, when th
446
446
  * `headers` {Object|string[]|null} The response headers. (optional)
447
447
  * `body` {Object|string|null} The response body. (optional)
448
448
 
449
+ ## Class: `ProxyConnectionError`
450
+
451
+ <!-- YAML
452
+ added: v8.10.1
453
+ changes:
454
+ - version: v8.10.1
455
+ pr-url: https://github.com/nodejs/undici/pull/5707
456
+ description: Added to fail the request instead of retrying forever when the proxy connection is torn down.
457
+ -->
458
+
459
+ * Extends: {UndiciError}
460
+
461
+ A connection to the proxy failed in a way that cannot be recovered on the
462
+ same connection, so the request fails instead of being retried.
463
+
464
+ * `name` {string} Always `'ProxyConnectionError'`.
465
+ * `code` {string} Always `'UND_ERR_PRX_CONN'`.
466
+ * `cause` {Error} The underlying error that caused the proxy connection to fail.
467
+
468
+ ### `new ProxyConnectionError(cause[, message[, options]])`
469
+
470
+ * `cause` {Error} The underlying error. (optional)
471
+ * `message` {string} The error message. (optional)
472
+ * `options` {Object} Additional `Error` options merged with `cause`. (optional)
473
+
449
474
  ## Class: `SecureProxyConnectionError`
450
475
 
451
476
  <!-- YAML
@@ -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
@@ -210,9 +210,10 @@ Automatically decompresses response bodies encoded with `gzip`, `x-gzip`,
210
210
  skipped. **Default:** `[204, 304]`.
211
211
  * `skipErrorResponses` {boolean} When `true`, responses with a status code
212
212
  >= 400 are not decompressed. **Default:** `true`.
213
- * `maxSize` {number} Maximum decompressed response size in bytes. The request
214
- fails with a `ResponseExceededMaxSizeError` if the decoded body exceeds
215
- this limit. **Default:** `67108864` (64 MiB).
213
+ * `maxSize` {number} Maximum decompressed response size in bytes for each
214
+ decompression stage. The request fails with a
215
+ `ResponseExceededMaxSizeError` if a stage exceeds this limit. Set to `0` to
216
+ disable the limit. **Default:** `0`.
216
217
 
217
218
  **Returns:** {Dispatcher.DispatcherComposeInterceptor}
218
219
 
@@ -73,12 +73,11 @@ connector shared by every pooled client.
73
73
 
74
74
  > [!NOTE]
75
75
  > `Pool` inherits all {ClientOptions}, including `allowH2` and
76
- > `maxConcurrentStreams`. With the default unlimited `connections`, the pool
77
- > opens a new client - and therefore a new TCP/TLS socket - per concurrent
78
- > dispatch, which defeats HTTP/2 multiplexing over a shared session. To benefit
79
- > from h2 multiplexing on a single session, cap `connections` (for example
80
- > `connections: 1`) so that concurrent requests share a session up to
81
- > `maxConcurrentStreams`.
76
+ > `maxConcurrentStreams`. For an h2-capable HTTPS origin, the pool waits for
77
+ > the first TLS connection to finish ALPN negotiation before opening more
78
+ > clients. If the server selects h2, concurrent requests share that session up
79
+ > to `maxConcurrentStreams`. If it selects HTTP/1.1, the pool resumes normal
80
+ > connection fan-out up to `connections`.
82
81
 
83
82
  ```mjs
84
83
  import { Pool } from 'undici'
@@ -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
- > Calling `body.formData()` on a fetch response causes undici to buffer and parse the entire body. Since this is dictated by the spec, `body.formData()` must only be called on responses from trusted servers.
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
 
@@ -5,7 +5,6 @@ const { addAbortListener } = require('node:events')
5
5
  const { Readable } = require('node:stream')
6
6
  const { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError } = require('../core/errors')
7
7
  const util = require('../core/util')
8
- const { ReadableStreamFrom } = require('../core/util')
9
8
 
10
9
  const kConsume = Symbol('kConsume')
11
10
  const kReading = Symbol('kReading')
@@ -246,7 +245,7 @@ class BodyReadable extends Readable {
246
245
  */
247
246
  get body () {
248
247
  if (!this[kBody]) {
249
- this[kBody] = ReadableStreamFrom(this)
248
+ this[kBody] = ReadableStream.from(this)
250
249
  if (this[kConsume]) {
251
250
  // TODO: Is this the best way to force a lock?
252
251
  this[kBody].getReader() // Ensure stream is locked.
@@ -368,10 +368,22 @@ class Request {
368
368
  }
369
369
  }
370
370
 
371
- onRequestUpgrade (statusCode, headers, socket) {
371
+ /**
372
+ * @param {number} statusCode
373
+ * @param {Buffer[]|string[]|import('../../types/header.d.ts').IncomingHttpHeaders} headers
374
+ * @param {import('node:stream').Duplex} socket
375
+ * @param {string} [statusText]
376
+ */
377
+ onRequestUpgrade (statusCode, headers, socket, statusText = '') {
378
+ this.onFinally()
379
+
372
380
  assert(!this.aborted)
373
381
  assert(!this.completed)
374
382
 
383
+ if (channels.headers.hasSubscribers) {
384
+ channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
385
+ }
386
+
375
387
  const controller = this[kController]
376
388
  if (controller) {
377
389
  controller.rawHeaders = headers
@@ -379,7 +391,16 @@ class Request {
379
391
 
380
392
  const parsedHeaders = Array.isArray(headers) ? parseHeaders(headers) : headers
381
393
 
382
- return this[kHandler].onRequestUpgrade?.(controller, statusCode, parsedHeaders, socket)
394
+ const result = this[kHandler].onRequestUpgrade?.(controller, statusCode, parsedHeaders, socket)
395
+
396
+ if (!this.aborted) {
397
+ this.completed = true
398
+ if (channels.trailers.hasSubscribers) {
399
+ channels.trailers.publish({ request: this, trailers: [] })
400
+ }
401
+ }
402
+
403
+ return result
383
404
  }
384
405
 
385
406
  onResponseEnd (trailers) {
package/lib/core/util.js CHANGED
@@ -644,44 +644,6 @@ function getSocketInfo (socket) {
644
644
  }
645
645
  }
646
646
 
647
- /**
648
- * @param {Iterable} iterable
649
- * @returns {ReadableStream}
650
- */
651
- function ReadableStreamFrom (iterable) {
652
- // We cannot use ReadableStream.from here because it does not return a byte stream.
653
-
654
- let iterator
655
- return new ReadableStream(
656
- {
657
- start () {
658
- iterator = iterable[Symbol.asyncIterator]()
659
- },
660
- pull (controller) {
661
- return iterator.next().then(({ done, value }) => {
662
- if (done) {
663
- return queueMicrotask(() => {
664
- controller.close()
665
- controller.byobRequest?.respond(0)
666
- })
667
- } else {
668
- const buf = Buffer.isBuffer(value) ? value : Buffer.from(value)
669
- if (buf.byteLength) {
670
- return controller.enqueue(new Uint8Array(buf))
671
- } else {
672
- return this.pull(controller)
673
- }
674
- }
675
- })
676
- },
677
- cancel () {
678
- return iterator.return()
679
- },
680
- type: 'bytes'
681
- }
682
- )
683
- }
684
-
685
647
  /**
686
648
  * The object should be a FormData instance and contains all the required
687
649
  * methods.
@@ -1026,7 +988,6 @@ module.exports = {
1026
988
  destroy,
1027
989
  bodyLength,
1028
990
  deepClone,
1029
- ReadableStreamFrom,
1030
991
  isBuffer,
1031
992
  assertRequestHandler,
1032
993
  getSocketInfo,
@@ -70,8 +70,14 @@ function lazyllhttp () {
70
70
 
71
71
  let mod
72
72
 
73
- // We disable wasm SIMD on ppc64 as it seems to be broken on Power 9 architectures.
74
- let useWasmSIMD = process.arch !== 'ppc64'
73
+ // We disable wasm SIMD on older versions of Node.js on ppc64 that are broken on Power >=9 architectures.
74
+ let useWasmSIMD = true
75
+ if (process.arch === 'ppc64') {
76
+ const [major, minor] = process.versions.node.split('.').map(n => parseInt(n, 10))
77
+ if (major < 24 || (major === 24 && minor < 12)) {
78
+ useWasmSIMD = false
79
+ }
80
+ }
75
81
  // The Env Variable UNDICI_NO_WASM_SIMD allows explicitly overriding the default behavior
76
82
  if (process.env.UNDICI_NO_WASM_SIMD === '1') {
77
83
  useWasmSIMD = false
@@ -561,7 +567,7 @@ class Parser {
561
567
  * @param {Buffer} head
562
568
  */
563
569
  onUpgrade (head) {
564
- const { upgrade, client, socket, headers, statusCode } = this
570
+ const { upgrade, client, socket, headers, statusCode, statusText } = this
565
571
 
566
572
  assert(upgrade)
567
573
  assert(client[kSocket] === socket)
@@ -596,8 +602,9 @@ class Parser {
596
602
  client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade'))
597
603
 
598
604
  try {
599
- request.onRequestUpgrade(statusCode, headers, socket)
605
+ request.onRequestUpgrade(statusCode, headers, socket, statusText)
600
606
  } catch (err) {
607
+ util.errorRequest(client, request, err)
601
608
  util.destroy(socket, err)
602
609
  }
603
610
 
@@ -5,6 +5,7 @@ const { pipeline } = require('node:stream')
5
5
  const util = require('../core/util.js')
6
6
  const {
7
7
  RequestContentLengthMismatchError,
8
+ ResponseContentLengthMismatchError,
8
9
  RequestAbortedError,
9
10
  SocketError,
10
11
  InformationalError,
@@ -193,6 +194,11 @@ function canReplayRequest (request) {
193
194
  return body == null || util.isBuffer(body) || util.isBlobLike(body)
194
195
  }
195
196
 
197
+ function hasResponseStarted (request) {
198
+ const state = request[kRequestStream]?.[kRequestStreamState]
199
+ return state?.responseReceived === true
200
+ }
201
+
196
202
  // Count a GOAWAY refusal against the request's replay budget. A peer that
197
203
  // refuses every connection must eventually surface an error to the caller
198
204
  // rather than being retried forever. Kept separate from canReplayRequest so
@@ -406,7 +412,8 @@ function resumeH2 (client) {
406
412
  const session = client[kHTTP2Session]
407
413
 
408
414
  if (socket?.destroyed === false) {
409
- if (client[kSize] === 0 || client[kMaxConcurrentStreams] === 0) {
415
+ // After an upgrade the queue is empty but its stream is still in use, so never unref while a stream is open.
416
+ if (session[kOpenStreams] === 0 && client[kSize] === 0) {
410
417
  unrefH2Session(session)
411
418
  } else {
412
419
  refH2Session(session)
@@ -640,9 +647,12 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
640
647
  const request = client[kQueue][i]
641
648
 
642
649
  if (request != null) {
650
+ // Read before detaching, which drops the stream state.
651
+ const responseStarted = hasResponseStarted(request)
652
+
643
653
  streamsToClose.push(detachRequestStreamForClose(request))
644
654
 
645
- if (canReplayRequest(request) && registerGoAwayRefusal(request)) {
655
+ if (!responseStarted && canReplayRequest(request) && registerGoAwayRefusal(request)) {
646
656
  retriableRequests.push(request)
647
657
  } else {
648
658
  util.errorRequest(client, request, err)
@@ -770,10 +780,12 @@ function noop () {}
770
780
 
771
781
  function closeStreamSession (stream) {
772
782
  const session = stream[kHTTP2Session]
783
+ const client = session[kClient]
773
784
 
774
785
  stream[kHTTP2Session] = null
775
786
  session[kOpenStreams] -= 1
776
- if (session[kOpenStreams] === 0) {
787
+ // A session that received GOAWAY does not need to stay ref'd for queued requests.
788
+ if (session[kOpenStreams] === 0 && (client[kSize] === 0 || session[kReceivedGoAway])) {
777
789
  unrefH2Session(session)
778
790
  setHttp2IdleTimeout(session)
779
791
  }
@@ -931,9 +943,14 @@ function onUpgradeResponse (headers, _flags) {
931
943
  const statusCode = headers[HTTP2_HEADER_STATUS]
932
944
  delete headers[HTTP2_HEADER_STATUS]
933
945
 
934
- request.onRequestUpgrade(statusCode, headers, stream)
946
+ try {
947
+ request.onRequestUpgrade(statusCode, headers, stream)
948
+ } catch (err) {
949
+ state.abort(err)
950
+ return
951
+ }
935
952
 
936
- if (request.aborted || request.completed) {
953
+ if (request.aborted) {
937
954
  return
938
955
  }
939
956
 
@@ -1033,6 +1050,7 @@ function writeH2 (client, request) {
1033
1050
  headersTimeout,
1034
1051
  bodyTimeout,
1035
1052
  requestFinalized: false,
1053
+ responseContentLength: null,
1036
1054
  responseReceived: false,
1037
1055
  bodySent: false,
1038
1056
  pendingEnd: false,
@@ -1328,12 +1346,17 @@ function onData (chunk) {
1328
1346
  return
1329
1347
  }
1330
1348
 
1331
- const { request, maxResponseSize } = state
1349
+ const { request, maxResponseSize, responseContentLength } = state
1332
1350
 
1333
1351
  if (request.aborted || request.completed) {
1334
1352
  return
1335
1353
  }
1336
1354
 
1355
+ if (responseContentLength != null && state.bytesRead + chunk.length > responseContentLength) {
1356
+ state.abort(new ResponseContentLengthMismatchError())
1357
+ return
1358
+ }
1359
+
1337
1360
  if (maxResponseSize > -1 && state.bytesRead + chunk.length > maxResponseSize) {
1338
1361
  // Unlike HTTP/1.1, which destroys the socket because it cannot abandon one
1339
1362
  // response without losing framing, resetting the offending stream leaves
@@ -1392,11 +1415,20 @@ function onResponse (headers) {
1392
1415
  stream.end()
1393
1416
  }
1394
1417
 
1395
- const statusCode = headers[HTTP2_HEADER_STATUS]
1418
+ const statusCode = Number(headers[HTTP2_HEADER_STATUS])
1396
1419
  delete headers[HTTP2_HEADER_STATUS]
1397
1420
  request.onResponseStarted()
1398
1421
  state.responseReceived = true
1399
1422
 
1423
+ // A Content-Length in HEAD and 304 responses describes the selected
1424
+ // representation rather than DATA on this stream. Successful CONNECT uses
1425
+ // the upgrade path above; all other final responses use Content-Length as
1426
+ // their DATA payload length.
1427
+ if (request.method !== 'HEAD' && statusCode !== 304) {
1428
+ const contentLength = headers[HTTP2_HEADER_CONTENT_LENGTH]
1429
+ state.responseContentLength = contentLength == null ? null : Number(contentLength)
1430
+ }
1431
+
1400
1432
  if (state.headersTimeout || state.bodyTimeout) {
1401
1433
  stream.setTimeout(state.bodyTimeout)
1402
1434
  }
@@ -1414,7 +1446,7 @@ function onResponse (headers) {
1414
1446
  return
1415
1447
  }
1416
1448
 
1417
- if (request.onResponseStart(Number(statusCode), headers, stream.resume.bind(stream), '') === false) {
1449
+ if (request.onResponseStart(statusCode, headers, stream.resume.bind(stream), '') === false) {
1418
1450
  stream.pause()
1419
1451
  }
1420
1452
 
@@ -1437,6 +1469,11 @@ function onEnd () {
1437
1469
  // trailers on the state by now, so completing here still delivers them.
1438
1470
  if (state.responseReceived) {
1439
1471
  if (!request.aborted && !request.completed) {
1472
+ if (state.responseContentLength != null && state.bytesRead !== state.responseContentLength) {
1473
+ state.abort(new ResponseContentLengthMismatchError())
1474
+ return
1475
+ }
1476
+
1440
1477
  state.pendingEnd = true
1441
1478
 
1442
1479
  // Complete on 'end': a blocked event loop can keep the stream's 'close'
@@ -1493,6 +1530,13 @@ function onError (err) {
1493
1530
 
1494
1531
  stream.off('error', onError)
1495
1532
 
1533
+ // Node's HTTP/2 implementation can turn an incomplete Content-Length body
1534
+ // into a protocol stream error instead of emitting 'end'. Prefer the
1535
+ // content-length mismatch error when the received byte count proves it.
1536
+ if (state.responseContentLength != null && state.bytesRead !== state.responseContentLength) {
1537
+ err = new ResponseContentLengthMismatchError()
1538
+ }
1539
+
1496
1540
  if (typeof stream.rstCode === 'number' && stream.rstCode !== NGHTTP2_NO_ERROR) {
1497
1541
  err.http2ErrorCode = stream.rstCode
1498
1542
  }
@@ -238,7 +238,7 @@ class Client extends DispatcherBase {
238
238
  throw new InvalidArgumentError('h2Options.settings.initialWindowSize must be a positive integer, greater than 0')
239
239
  }
240
240
 
241
- if (h2Options.maxConcurrentStreams != null && (!Number.isInteger(h2Options.connectionWindowSize) || h2Options.maxConcurrentStreams < 1)) {
241
+ if (h2Options.maxConcurrentStreams != null && (!Number.isInteger(h2Options.maxConcurrentStreams) || h2Options.maxConcurrentStreams < 1)) {
242
242
  throw new InvalidArgumentError('h2Options.maxConcurrentStreams must be a positive integer, greater than 0')
243
243
  }
244
244
 
@@ -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
@@ -94,17 +94,25 @@ class EnvHttpProxyAgent extends DispatcherBase {
94
94
  if (this.#noProxyEntries.length === 0) {
95
95
  return true // Always proxy if NO_PROXY is not set or empty.
96
96
  }
97
- if (this.#noProxyValue === '*') {
98
- return false // Never proxy if wildcard is set.
99
- }
100
97
 
101
98
  for (let i = 0; i < this.#noProxyEntries.length; i++) {
102
99
  const entry = this.#noProxyEntries[i]
100
+ // A bare `*` entry matches all hosts regardless of its position or the
101
+ // surrounding whitespace (e.g. ` * ` or `none.invalid,*`). If a port is
102
+ // attached (`*:80`) it only bypasses that port.
103
+ if (entry.hostname === '*') {
104
+ if (entry.port && entry.port !== port) {
105
+ continue
106
+ }
107
+ return false // Never proxy if a wildcard entry is present.
108
+ }
103
109
  if (entry.port && entry.port !== port) {
104
110
  continue // Skip if ports don't match.
105
111
  }
106
- // Don't proxy if the hostname is equal with the no_proxy host.
107
- if (hostname === entry.hostname) {
112
+ // Don't proxy if the hostname is equal with the no_proxy host. A
113
+ // `*.example.com` wildcard matches subdomains only, not the apex
114
+ // `example.com`, so exact matches are skipped for wildcard entries.
115
+ if (!entry.wildcard && hostname === entry.hostname) {
108
116
  return false
109
117
  }
110
118
  // Don't proxy if the hostname is the subdomain of the no_proxy host.
@@ -149,10 +157,17 @@ class EnvHttpProxyAgent extends DispatcherBase {
149
157
  port = parsed ? Number.parseInt(parsed[2], 10) : 0
150
158
  }
151
159
 
160
+ // A leading `*` marks a subdomain wildcard (`*.example.com`), distinct
161
+ // from a plain or leading-dot suffix (`example.com` / `.example.com`)
162
+ // which also matches the apex. `*.example.com` must only match
163
+ // subdomains, never the apex `example.com` itself.
164
+ const wildcard = entry.charCodeAt(0) === 42 /* '*' */
165
+
152
166
  noProxyEntries.push({
153
167
  // strip leading dot or asterisk with dot, and any trailing dot
154
168
  hostname: hostname.replace(/^\*?\./, '').replace(/^(.+)\.$/, '$1').toLowerCase(),
155
- port
169
+ port,
170
+ wildcard
156
171
  })
157
172
  }
158
173