@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.
- package/README.md +2 -2
- package/changes.json +1 -1
- package/docs/docs/api/Agent.md +8 -8
- package/docs/docs/api/DiagnosticsChannel.md +9 -6
- package/docs/docs/api/Dispatcher.md +9 -0
- package/docs/docs/api/Errors.md +25 -0
- package/docs/docs/api/Fetch.md +10 -0
- package/docs/docs/api/Interceptors.md +4 -3
- package/docs/docs/api/Pool.md +5 -6
- package/docs/docs/index.md +10 -1
- package/lib/api/readable.js +1 -2
- package/lib/core/request.js +23 -2
- package/lib/core/util.js +0 -39
- package/lib/dispatcher/client-h1.js +11 -4
- package/lib/dispatcher/client-h2.js +52 -8
- package/lib/dispatcher/client.js +6 -1
- package/lib/dispatcher/env-http-proxy-agent.js +21 -6
- package/lib/dispatcher/pool-base.js +63 -2
- package/lib/dispatcher/pool.js +90 -7
- package/lib/dispatcher/proxy-agent.js +16 -3
- package/lib/handler/deduplication-handler.js +24 -11
- package/lib/handler/redirect-handler.js +5 -0
- package/lib/handler/retry-handler.js +72 -13
- package/lib/interceptor/cache.js +4 -1
- package/lib/interceptor/decompress.js +236 -69
- package/lib/web/cache/cache.js +17 -8
- package/lib/web/cache/cachestorage.js +11 -5
- package/lib/web/cookies/index.js +19 -7
- package/lib/web/cookies/util.js +3 -1
- package/lib/web/eventsource/eventsource.js +25 -1
- package/lib/web/fetch/body.js +22 -15
- package/lib/web/fetch/data-url.js +6 -6
- package/lib/web/fetch/formdata.js +30 -36
- package/lib/web/fetch/headers.js +34 -31
- package/lib/web/fetch/index.js +46 -25
- package/lib/web/fetch/request.js +74 -79
- package/lib/web/fetch/response.js +43 -44
- package/lib/web/fetch/util.js +7 -7
- package/lib/web/infra/index.js +16 -1
- package/lib/web/webidl/index.js +2 -14
- package/lib/web/websocket/connection.js +33 -12
- package/lib/web/websocket/events.js +43 -26
- package/lib/web/websocket/receiver.js +2 -0
- package/lib/web/websocket/stream/websocketerror.js +13 -10
- package/lib/web/websocket/stream/websocketstream.js +19 -4
- package/lib/web/websocket/websocket.js +44 -41
- package/package.json +3 -3
- package/types/diagnostics-channel.d.ts +2 -1
- package/types/interceptors.d.ts +1 -1
- 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.
|
|
17
|
-
| Processed | 2026-09-
|
|
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
package/docs/docs/api/Agent.md
CHANGED
|
@@ -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}).
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
>
|
|
66
|
-
>
|
|
67
|
-
>
|
|
68
|
-
>
|
|
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[]}
|
|
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
|
|
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
|
|
172
|
-
the
|
|
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
|
package/docs/docs/api/Errors.md
CHANGED
|
@@ -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
|
package/docs/docs/api/Fetch.md
CHANGED
|
@@ -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
|
|
214
|
-
|
|
215
|
-
this limit.
|
|
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
|
|
package/docs/docs/api/Pool.md
CHANGED
|
@@ -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`.
|
|
77
|
-
>
|
|
78
|
-
>
|
|
79
|
-
>
|
|
80
|
-
>
|
|
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'
|
package/docs/docs/index.md
CHANGED
|
@@ -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
|
-
>
|
|
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
|
|
package/lib/api/readable.js
CHANGED
|
@@ -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] =
|
|
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.
|
package/lib/core/request.js
CHANGED
|
@@ -368,10 +368,22 @@ class Request {
|
|
|
368
368
|
}
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
-
|
|
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
|
-
|
|
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
|
|
74
|
-
let useWasmSIMD =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
946
|
+
try {
|
|
947
|
+
request.onRequestUpgrade(statusCode, headers, stream)
|
|
948
|
+
} catch (err) {
|
|
949
|
+
state.abort(err)
|
|
950
|
+
return
|
|
951
|
+
}
|
|
935
952
|
|
|
936
|
-
if (request.aborted
|
|
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(
|
|
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
|
}
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|