undici 8.9.0 → 8.10.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/docs/docs/api/Client.md +21 -8
- package/lib/api/readable.js +33 -43
- package/lib/core/connect.js +16 -2
- package/lib/core/symbols.js +1 -0
- package/lib/dispatcher/client-h1.js +3 -3
- package/lib/dispatcher/client-h2.js +179 -10
- package/lib/dispatcher/client.js +74 -29
- package/lib/dispatcher/env-http-proxy-agent.js +28 -6
- package/lib/dispatcher/socks5-proxy-agent.js +15 -1
- package/lib/handler/retry-handler.js +7 -2
- package/lib/interceptor/cache.js +5 -2
- package/lib/interceptor/deduplicate.js +1 -1
- package/lib/mock/mock-utils.js +213 -24
- package/lib/util/cache.js +2 -4
- package/lib/web/websocket/websocket.js +22 -0
- package/package.json +1 -1
- package/types/client.d.ts +40 -0
package/docs/docs/api/Client.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
* `
|
|
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`.
|
package/lib/api/readable.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
}
|
package/lib/core/connect.js
CHANGED
|
@@ -105,13 +105,27 @@ function buildConnector ({ allowH2, preferH2, useH2c, maxCachedSessions, socketP
|
|
|
105
105
|
|
|
106
106
|
port = port || 80
|
|
107
107
|
|
|
108
|
-
|
|
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
|
}
|
package/lib/core/symbols.js
CHANGED
|
@@ -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'),
|
|
@@ -1052,7 +1052,7 @@ function onSocketClose () {
|
|
|
1052
1052
|
|
|
1053
1053
|
function clearIdleSocketValidation (socket) {
|
|
1054
1054
|
if (socket[kIdleSocketValidationTimeout]) {
|
|
1055
|
-
|
|
1055
|
+
clearTimeout(socket[kIdleSocketValidationTimeout])
|
|
1056
1056
|
socket[kIdleSocketValidationTimeout] = null
|
|
1057
1057
|
}
|
|
1058
1058
|
|
|
@@ -1061,14 +1061,14 @@ function clearIdleSocketValidation (socket) {
|
|
|
1061
1061
|
|
|
1062
1062
|
function scheduleIdleSocketValidation (client, socket) {
|
|
1063
1063
|
socket[kIdleSocketValidation] = 1
|
|
1064
|
-
socket[kIdleSocketValidationTimeout] =
|
|
1064
|
+
socket[kIdleSocketValidationTimeout] = setTimeout(() => {
|
|
1065
1065
|
socket[kIdleSocketValidationTimeout] = null
|
|
1066
1066
|
socket[kIdleSocketValidation] = 2
|
|
1067
1067
|
|
|
1068
1068
|
if (client[kSocket] === socket && !socket.destroyed) {
|
|
1069
1069
|
client[kResume]()
|
|
1070
1070
|
}
|
|
1071
|
-
})
|
|
1071
|
+
}, 0)
|
|
1072
1072
|
socket[kIdleSocketValidationTimeout].unref?.()
|
|
1073
1073
|
}
|
|
1074
1074
|
|
|
@@ -26,10 +26,7 @@ const {
|
|
|
26
26
|
kStrictContentLength,
|
|
27
27
|
kOnError,
|
|
28
28
|
kMaxConcurrentStreams,
|
|
29
|
-
kPingInterval,
|
|
30
29
|
kHTTP2Session,
|
|
31
|
-
kHTTP2InitialWindowSize,
|
|
32
|
-
kHTTP2ConnectionWindowSize,
|
|
33
30
|
kHostAuthority,
|
|
34
31
|
kResume,
|
|
35
32
|
kSize,
|
|
@@ -41,7 +38,8 @@ const {
|
|
|
41
38
|
kEnableConnectProtocol,
|
|
42
39
|
kRemoteSettings,
|
|
43
40
|
kHTTP2Stream,
|
|
44
|
-
kHTTP2SessionState
|
|
41
|
+
kHTTP2SessionState,
|
|
42
|
+
kHTTP2Options
|
|
45
43
|
} = require('../core/symbols.js')
|
|
46
44
|
const { channels } = require('../core/diagnostics.js')
|
|
47
45
|
|
|
@@ -51,6 +49,14 @@ const kRequestStream = Symbol('request stream')
|
|
|
51
49
|
const kRequestStreamCleanup = Symbol('request stream cleanup')
|
|
52
50
|
const kRequestStreamState = Symbol('request stream state')
|
|
53
51
|
const kReceivedGoAway = Symbol('received goaway')
|
|
52
|
+
const kGoAwayReplayAttempts = Symbol('goaway replay attempts')
|
|
53
|
+
const kRefusedStreamRetry = Symbol('refused stream retry')
|
|
54
|
+
|
|
55
|
+
// RFC 9113 section 8.7: a client SHOULD NOT automatically retry a request more
|
|
56
|
+
// than once. Without a budget a peer that keeps refusing turns one request into
|
|
57
|
+
// an unbounded connect/refuse/reconnect loop that never settles and starves the
|
|
58
|
+
// event loop.
|
|
59
|
+
const MAX_GOAWAY_REPLAY_ATTEMPTS = 1
|
|
54
60
|
|
|
55
61
|
let extractBody
|
|
56
62
|
|
|
@@ -179,12 +185,24 @@ function completeRequest (client, request, resetPendingIdx = false) {
|
|
|
179
185
|
}
|
|
180
186
|
}
|
|
181
187
|
|
|
182
|
-
function
|
|
188
|
+
function canReplayRequest (request) {
|
|
183
189
|
const { body } = request
|
|
184
190
|
|
|
185
191
|
return body == null || util.isBuffer(body) || util.isBlobLike(body)
|
|
186
192
|
}
|
|
187
193
|
|
|
194
|
+
// Count a GOAWAY refusal against the request's replay budget. A peer that
|
|
195
|
+
// refuses every connection must eventually surface an error to the caller
|
|
196
|
+
// rather than being retried forever. Kept separate from canReplayRequest so
|
|
197
|
+
// that the REFUSED_STREAM retry, which has its own single-attempt limit, does
|
|
198
|
+
// not consume this budget just by asking whether the body can be replayed.
|
|
199
|
+
function registerGoAwayRefusal (request) {
|
|
200
|
+
const attempts = (request[kGoAwayReplayAttempts] ?? 0) + 1
|
|
201
|
+
request[kGoAwayReplayAttempts] = attempts
|
|
202
|
+
|
|
203
|
+
return attempts <= MAX_GOAWAY_REPLAY_ATTEMPTS
|
|
204
|
+
}
|
|
205
|
+
|
|
188
206
|
function closeStream (stream, code = NGHTTP2_REFUSED_STREAM) {
|
|
189
207
|
if (stream != null && !stream.destroyed && !stream.closed) {
|
|
190
208
|
try {
|
|
@@ -197,19 +215,44 @@ function detachRequestStreamForClose (request) {
|
|
|
197
215
|
const stream = request[kRequestStream]
|
|
198
216
|
|
|
199
217
|
clearRequestStream(request)
|
|
218
|
+
severRequestStream(stream)
|
|
200
219
|
|
|
201
220
|
return stream
|
|
202
221
|
}
|
|
203
222
|
|
|
223
|
+
// Unbind a stream from its request for good. releaseRequestStream() alone
|
|
224
|
+
// leaves the 'close' listener attached and kRequestStreamState populated, so a
|
|
225
|
+
// stream abandoned here would still run completeRequestStream() later — and
|
|
226
|
+
// splice out the request that has since been requeued onto another session.
|
|
227
|
+
function severRequestStream (stream) {
|
|
228
|
+
if (stream == null || stream[kRequestStreamState] == null) {
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
stream[kRequestStreamState] = null
|
|
233
|
+
stream.off('close', completeRequestStream)
|
|
234
|
+
// Upgrade streams use their own close cleanup, which would otherwise release
|
|
235
|
+
// the session a second time after the stream has been severed for GOAWAY.
|
|
236
|
+
stream.off('close', onUpgradeStreamClose)
|
|
237
|
+
|
|
238
|
+
if (stream[kHTTP2Session] != null) {
|
|
239
|
+
closeStreamSession(stream)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!stream.destroyed && !stream.closed) {
|
|
243
|
+
stream.once('error', noop)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
204
247
|
function connectH2 (client, socket) {
|
|
205
248
|
client[kSocket] = socket
|
|
206
249
|
|
|
207
|
-
const http2InitialWindowSize = client[
|
|
208
|
-
const http2ConnectionWindowSize = client[
|
|
250
|
+
const http2InitialWindowSize = client[kHTTP2Options].sessionOptions?.initialWindowSize
|
|
251
|
+
const http2ConnectionWindowSize = client[kHTTP2Options].connectionWindowSize
|
|
209
252
|
|
|
210
253
|
const session = http2.connect(client[kUrl], {
|
|
211
254
|
createConnection: () => socket,
|
|
212
|
-
peerMaxConcurrentStreams: client[
|
|
255
|
+
peerMaxConcurrentStreams: client[kHTTP2Options].maxConcurrentStreams,
|
|
213
256
|
settings: {
|
|
214
257
|
// TODO(metcoder95): add support for PUSH
|
|
215
258
|
enablePush: false,
|
|
@@ -223,13 +266,16 @@ function connectH2 (client, socket) {
|
|
|
223
266
|
session[kSocket] = socket
|
|
224
267
|
session[kHTTP2SessionState] = {
|
|
225
268
|
idleTimeout: null,
|
|
269
|
+
// Armed while the peer advertises MAX_CONCURRENT_STREAMS = 0 and we have
|
|
270
|
+
// work that cannot start. See setNoStreamsTimeout.
|
|
271
|
+
noStreamsTimeout: null,
|
|
226
272
|
// Sockets start out ref'd. Session ref/unref proxies to the socket, so a
|
|
227
273
|
// single cached flag lets us skip redundant uv ref/unref calls, provided
|
|
228
274
|
// every ref/unref of the session or its socket goes through
|
|
229
275
|
// refH2Session/unrefH2Session.
|
|
230
276
|
refed: true,
|
|
231
277
|
ping: {
|
|
232
|
-
interval: client[
|
|
278
|
+
interval: client[kHTTP2Options].pingInterval === 0 ? null : setInterval(onHttp2SendPing, client[kHTTP2Options].pingInterval, session).unref()
|
|
233
279
|
}
|
|
234
280
|
}
|
|
235
281
|
session[kReceivedGoAway] = false
|
|
@@ -369,7 +415,74 @@ function resumeH2 (client) {
|
|
|
369
415
|
} else {
|
|
370
416
|
clearHttp2IdleTimeout(session)
|
|
371
417
|
}
|
|
418
|
+
|
|
419
|
+
if (client[kMaxConcurrentStreams] === 0 && client[kRunning] === 0 && client[kPending] > 0) {
|
|
420
|
+
setNoStreamsTimeout(session)
|
|
421
|
+
} else {
|
|
422
|
+
clearNoStreamsTimeout(session)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function clearNoStreamsTimeout (session) {
|
|
428
|
+
const state = session[kHTTP2SessionState]
|
|
429
|
+
|
|
430
|
+
if (state?.noStreamsTimeout != null) {
|
|
431
|
+
clearTimeout(state.noStreamsTimeout)
|
|
432
|
+
state.noStreamsTimeout = null
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// A peer is allowed to advertise SETTINGS_MAX_CONCURRENT_STREAMS = 0 to refuse
|
|
437
|
+
// new streams (RFC 9113 §6.5.2), and is expected to raise it again later. Until
|
|
438
|
+
// it does, busy() reports the client as permanently busy and queued requests
|
|
439
|
+
// cannot open a stream — which means no per-stream timeout covers them, and no
|
|
440
|
+
// reconnect can happen either, so the SETTINGS frame that would lift the limit
|
|
441
|
+
// can never arrive. Give the peer headersTimeout to start honouring requests
|
|
442
|
+
// before failing them; a request that cannot even be sent has missed the same
|
|
443
|
+
// deadline as one whose headers never arrive.
|
|
444
|
+
function setNoStreamsTimeout (session) {
|
|
445
|
+
const client = session[kClient]
|
|
446
|
+
const state = session[kHTTP2SessionState]
|
|
447
|
+
const timeout = client[kHeadersTimeout]
|
|
448
|
+
|
|
449
|
+
if (!timeout || state.noStreamsTimeout != null) {
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
state.noStreamsTimeout = setTimeout(onNoStreamsTimeout, timeout, session).unref()
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function onNoStreamsTimeout (session) {
|
|
457
|
+
const client = session[kClient]
|
|
458
|
+
const state = session[kHTTP2SessionState]
|
|
459
|
+
|
|
460
|
+
state.noStreamsTimeout = null
|
|
461
|
+
|
|
462
|
+
if (
|
|
463
|
+
client[kHTTP2Session] !== session ||
|
|
464
|
+
client[kMaxConcurrentStreams] !== 0 ||
|
|
465
|
+
client[kRunning] !== 0 ||
|
|
466
|
+
client[kPending] === 0
|
|
467
|
+
) {
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const err = new HeadersTimeoutError(
|
|
472
|
+
`HTTP/2: server did not accept a new stream within ${client[kHeadersTimeout]}`
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
const requests = client[kQueue].splice(client[kPendingIdx])
|
|
476
|
+
for (let i = 0; i < requests.length; i++) {
|
|
477
|
+
if (requests[i] != null) {
|
|
478
|
+
util.errorRequest(client, requests[i], err)
|
|
479
|
+
}
|
|
372
480
|
}
|
|
481
|
+
|
|
482
|
+
// Drop the unusable session so the next request gets a fresh connection,
|
|
483
|
+
// whose SETTINGS may well allow streams again.
|
|
484
|
+
session[kError] = err
|
|
485
|
+
resetHttp2Session(session, err)
|
|
373
486
|
}
|
|
374
487
|
|
|
375
488
|
function clearHttp2IdleTimeout (session) {
|
|
@@ -527,7 +640,7 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
|
|
|
527
640
|
if (request != null) {
|
|
528
641
|
streamsToClose.push(detachRequestStreamForClose(request))
|
|
529
642
|
|
|
530
|
-
if (
|
|
643
|
+
if (canReplayRequest(request) && registerGoAwayRefusal(request)) {
|
|
531
644
|
retriableRequests.push(request)
|
|
532
645
|
} else {
|
|
533
646
|
util.errorRequest(client, request, err)
|
|
@@ -552,6 +665,7 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
|
|
|
552
665
|
}
|
|
553
666
|
|
|
554
667
|
clearHttp2IdleTimeout(this)
|
|
668
|
+
clearNoStreamsTimeout(this)
|
|
555
669
|
|
|
556
670
|
if (!this.closed && !this.destroyed) {
|
|
557
671
|
this.close()
|
|
@@ -576,6 +690,7 @@ function onHttp2SessionClose () {
|
|
|
576
690
|
}
|
|
577
691
|
|
|
578
692
|
clearHttp2IdleTimeout(this)
|
|
693
|
+
clearNoStreamsTimeout(this)
|
|
579
694
|
|
|
580
695
|
if (state.ping.interval != null) {
|
|
581
696
|
clearInterval(state.ping.interval)
|
|
@@ -687,6 +802,16 @@ function completeRequestStream () {
|
|
|
687
802
|
|
|
688
803
|
if (state.pendingEnd && !state.request.aborted && !state.request.completed) {
|
|
689
804
|
state.request.onResponseEnd(state.trailers || {})
|
|
805
|
+
} else if (!state.request.aborted && !state.request.completed) {
|
|
806
|
+
// The stream closed without a complete response and without reporting an
|
|
807
|
+
// error. finalizeRequest() below frees the queue slot either way, so
|
|
808
|
+
// without this the request would simply vanish and its caller would never
|
|
809
|
+
// hear back.
|
|
810
|
+
util.errorRequest(
|
|
811
|
+
state.client,
|
|
812
|
+
state.request,
|
|
813
|
+
new InformationalError('HTTP/2: stream closed before the response was complete')
|
|
814
|
+
)
|
|
690
815
|
}
|
|
691
816
|
|
|
692
817
|
finalizeRequest(state)
|
|
@@ -1286,6 +1411,38 @@ function onEnd () {
|
|
|
1286
1411
|
}
|
|
1287
1412
|
}
|
|
1288
1413
|
|
|
1414
|
+
function retryRefusedStream (stream, state) {
|
|
1415
|
+
const { client, request } = state
|
|
1416
|
+
|
|
1417
|
+
if (
|
|
1418
|
+
state.responseReceived ||
|
|
1419
|
+
request.aborted ||
|
|
1420
|
+
request.completed ||
|
|
1421
|
+
request[kRefusedStreamRetry] ||
|
|
1422
|
+
!canReplayRequest(request)
|
|
1423
|
+
) {
|
|
1424
|
+
return false
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// RFC 9113 section 8.7 permits retrying REFUSED_STREAM, but says clients
|
|
1428
|
+
// SHOULD NOT automatically retry the same request more than once.
|
|
1429
|
+
request[kRefusedStreamRetry] = true
|
|
1430
|
+
|
|
1431
|
+
// Detach the failed attempt before moving the request back to the pending
|
|
1432
|
+
// queue. The peer only reset this stream, so the HTTP/2 session remains
|
|
1433
|
+
// usable for the retry. Severing also drops the 'close' listener, so the
|
|
1434
|
+
// abandoned stream cannot later complete the retried request.
|
|
1435
|
+
detachRequestStreamForClose(request)
|
|
1436
|
+
state.stream = null
|
|
1437
|
+
state.requestFinalized = true
|
|
1438
|
+
|
|
1439
|
+
completeRequest(client, request)
|
|
1440
|
+
client[kQueue].splice(client[kPendingIdx], 0, request)
|
|
1441
|
+
client[kResume]()
|
|
1442
|
+
|
|
1443
|
+
return true
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1289
1446
|
function onError (err) {
|
|
1290
1447
|
const stream = this
|
|
1291
1448
|
const state = stream[kRequestStreamState]
|
|
@@ -1295,6 +1452,18 @@ function onError (err) {
|
|
|
1295
1452
|
}
|
|
1296
1453
|
|
|
1297
1454
|
stream.off('error', onError)
|
|
1455
|
+
|
|
1456
|
+
if (typeof stream.rstCode === 'number' && stream.rstCode !== NGHTTP2_NO_ERROR) {
|
|
1457
|
+
err.http2ErrorCode = stream.rstCode
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
if (
|
|
1461
|
+
stream.rstCode === NGHTTP2_REFUSED_STREAM &&
|
|
1462
|
+
retryRefusedStream(stream, state)
|
|
1463
|
+
) {
|
|
1464
|
+
return
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1298
1467
|
state.abort(err)
|
|
1299
1468
|
}
|
|
1300
1469
|
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -53,10 +53,8 @@ const {
|
|
|
53
53
|
kHTTPContext,
|
|
54
54
|
kMaxConcurrentStreams,
|
|
55
55
|
kHostAuthority,
|
|
56
|
-
kHTTP2InitialWindowSize,
|
|
57
|
-
kHTTP2ConnectionWindowSize,
|
|
58
56
|
kResume,
|
|
59
|
-
|
|
57
|
+
kHTTP2Options
|
|
60
58
|
} = require('../core/symbols.js')
|
|
61
59
|
const connectH1 = require('./client-h1.js')
|
|
62
60
|
const connectH2 = require('./client-h2.js')
|
|
@@ -76,6 +74,16 @@ function getPipelining (client) {
|
|
|
76
74
|
return client[kPipelining] ?? client[kHTTPContext]?.defaultPipelining ?? 1
|
|
77
75
|
}
|
|
78
76
|
|
|
77
|
+
let h2NamespaceOptsWarning = false
|
|
78
|
+
function emitH2OptionsNamespaceWarning (optName) {
|
|
79
|
+
if (h2NamespaceOptsWarning === true) return
|
|
80
|
+
|
|
81
|
+
process.emitWarning(`Use h2Options.${optName} instead. ${optName} for H2 will be deprecated in future major.`, {
|
|
82
|
+
code: 'UNDICI-H2-OPTIONS'
|
|
83
|
+
})
|
|
84
|
+
h2NamespaceOptsWarning = true
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
// Protocol-aware dispatch ceiling. h1 RFC7230 pipelining is unrelated to h2
|
|
80
88
|
// stream multiplexing — over h2 the ceiling is the (server-confirmed)
|
|
81
89
|
// maxConcurrentStreams. Before a context is attached we use the h1
|
|
@@ -128,7 +136,8 @@ class Client extends DispatcherBase {
|
|
|
128
136
|
initialWindowSize,
|
|
129
137
|
connectionWindowSize,
|
|
130
138
|
pingInterval,
|
|
131
|
-
webSocket
|
|
139
|
+
webSocket,
|
|
140
|
+
h2Options
|
|
132
141
|
} = {}) {
|
|
133
142
|
if (keepAlive !== undefined) {
|
|
134
143
|
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
|
|
@@ -216,24 +225,55 @@ class Client extends DispatcherBase {
|
|
|
216
225
|
throw new InvalidArgumentError('allowH2 must be a valid boolean value')
|
|
217
226
|
}
|
|
218
227
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
228
|
+
// We validate only if allowH2 is enabled or null (enabled by default)
|
|
229
|
+
if (allowH2 !== false) {
|
|
230
|
+
// Prioritise new h2Options object, otherwise fallback to prior configuration options
|
|
231
|
+
if (h2Options != null) {
|
|
232
|
+
if (h2Options.useH2c != null && typeof h2Options.useH2c !== 'boolean') {
|
|
233
|
+
throw new InvalidArgumentError('h2Options.useH2c must be a valid boolean value')
|
|
234
|
+
}
|
|
222
235
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
236
|
+
if (h2Options.settings?.initialWindowSize != null && (!Number.isInteger(h2Options.settings.initialWindowSize) || h2Options.settings.initialWindowSize < 1)) {
|
|
237
|
+
throw new InvalidArgumentError('h2Options.settings.initialWindowSize must be a positive integer, greater than 0')
|
|
238
|
+
}
|
|
226
239
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
240
|
+
if (h2Options.maxConcurrentStreams != null && (!Number.isInteger(h2Options.connectionWindowSize) || h2Options.maxConcurrentStreams < 1)) {
|
|
241
|
+
throw new InvalidArgumentError('h2Options.maxConcurrentStreams must be a positive integer, greater than 0')
|
|
242
|
+
}
|
|
230
243
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
244
|
+
if (h2Options.connectionWindowSize != null && (!Number.isInteger(h2Options.connectionWindowSize) || h2Options.connectionWindowSize < 1)) {
|
|
245
|
+
throw new InvalidArgumentError('h2Options.connectionWindowSize must be a positive integer, greater than 0')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (h2Options.pingInterval != null && (typeof h2Options.pingInterval !== 'number' || !Number.isInteger(h2Options.pingInterval) || h2Options.pingInterval < 0)) {
|
|
249
|
+
throw new InvalidArgumentError('h2Options.pingInterval must be a positive integer, greater or equal to 0')
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
if (useH2c != null && typeof useH2c !== 'boolean') {
|
|
253
|
+
emitH2OptionsNamespaceWarning('useH2c')
|
|
254
|
+
throw new InvalidArgumentError('useH2c must be a valid boolean value')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== 'number' || maxConcurrentStreams < 1)) {
|
|
258
|
+
emitH2OptionsNamespaceWarning('maxConcurrentStreams')
|
|
259
|
+
throw new InvalidArgumentError('maxConcurrentStreams must be a positive integer, greater than 0')
|
|
260
|
+
}
|
|
234
261
|
|
|
235
|
-
|
|
236
|
-
|
|
262
|
+
if (initialWindowSize != null && (!Number.isInteger(initialWindowSize) || initialWindowSize < 1)) {
|
|
263
|
+
emitH2OptionsNamespaceWarning('initialWindowSize')
|
|
264
|
+
throw new InvalidArgumentError('initialWindowSize must be a positive integer, greater than 0')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (connectionWindowSize != null && (!Number.isInteger(connectionWindowSize) || connectionWindowSize < 1)) {
|
|
268
|
+
emitH2OptionsNamespaceWarning('connectionWindowSize')
|
|
269
|
+
throw new InvalidArgumentError('connectionWindowSize must be a positive integer, greater than 0')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (pingInterval != null && (typeof pingInterval !== 'number' || !Number.isInteger(pingInterval) || pingInterval < 0)) {
|
|
273
|
+
emitH2OptionsNamespaceWarning('pingInterval')
|
|
274
|
+
throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0')
|
|
275
|
+
}
|
|
276
|
+
}
|
|
237
277
|
}
|
|
238
278
|
|
|
239
279
|
super({ webSocket })
|
|
@@ -243,8 +283,8 @@ class Client extends DispatcherBase {
|
|
|
243
283
|
...tls,
|
|
244
284
|
maxCachedSessions,
|
|
245
285
|
allowH2,
|
|
246
|
-
useH2c,
|
|
247
286
|
socketPath,
|
|
287
|
+
useH2c: h2Options?.useH2c ?? useH2c,
|
|
248
288
|
timeout: connectTimeout,
|
|
249
289
|
...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
|
|
250
290
|
...connect
|
|
@@ -280,16 +320,20 @@ class Client extends DispatcherBase {
|
|
|
280
320
|
this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1
|
|
281
321
|
this[kHTTPContext] = null
|
|
282
322
|
// h2
|
|
283
|
-
this[
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
323
|
+
this[kHTTP2Options] = {
|
|
324
|
+
pingInterval: h2Options?.pingInterval ?? pingInterval ?? 60e3,
|
|
325
|
+
connectionWindowSize: h2Options?.connectionWindowSize ?? connectionWindowSize ?? 524288,
|
|
326
|
+
maxConcurrentStreams: h2Options?.maxConcurrentStreams ?? maxConcurrentStreams ?? 100, // Max peerConcurrentStreams for a Node h2 server
|
|
327
|
+
sessionOptions: {
|
|
328
|
+
// HTTP/2 window sizes are set to higher defaults than Node.js core for better performance:
|
|
329
|
+
// - initialWindowSize: 262144 (256KB) vs Node.js default 65535 (64KB - 1)
|
|
330
|
+
// Allows more data to be sent before requiring acknowledgment, improving throughput
|
|
331
|
+
// especially on high-latency networks. This matches common production HTTP/2 servers.
|
|
332
|
+
// - connectionWindowSize: 524288 (512KB) vs Node.js default (none set)
|
|
333
|
+
// Provides better flow control for the entire connection across multiple streams.
|
|
334
|
+
initialWindowSize: h2Options?.initialWindowSize ?? initialWindowSize ?? 262144
|
|
335
|
+
}
|
|
336
|
+
}
|
|
293
337
|
|
|
294
338
|
// kQueue is built up of 3 sections separated by
|
|
295
339
|
// the kRunningIdx and kPendingIdx indices.
|
|
@@ -672,6 +716,7 @@ function _resume (client, sync) {
|
|
|
672
716
|
}
|
|
673
717
|
|
|
674
718
|
if (!client[kHTTPContext]) {
|
|
719
|
+
client[kServerName] = request.servername
|
|
675
720
|
connect(client)
|
|
676
721
|
return
|
|
677
722
|
}
|
|
@@ -65,9 +65,10 @@ class EnvHttpProxyAgent extends DispatcherBase {
|
|
|
65
65
|
#getProxyAgentForUrl (url) {
|
|
66
66
|
let { protocol, host: hostname, port } = url
|
|
67
67
|
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
68
|
+
// Remove the port suffix (e.g. ":8080") and then strip surrounding
|
|
69
|
+
// brackets from IPv6 literals (e.g. "[::1]" -> "::1") so that the
|
|
70
|
+
// result matches the unbracketed form stored by #parseNoProxy.
|
|
71
|
+
hostname = hostname.replace(/:\d*$/, '').replace(/^\[(.+)\]$/, '$1').toLowerCase()
|
|
71
72
|
port = Number.parseInt(port, 10) || DEFAULT_PORTS[protocol] || 0
|
|
72
73
|
if (!this.#shouldProxy(hostname, port)) {
|
|
73
74
|
return this[kNoProxyAgent]
|
|
@@ -119,11 +120,32 @@ class EnvHttpProxyAgent extends DispatcherBase {
|
|
|
119
120
|
if (!entry) {
|
|
120
121
|
continue
|
|
121
122
|
}
|
|
122
|
-
|
|
123
|
+
|
|
124
|
+
// An IPv6 entry with a port must be bracketed: [::1]:443.
|
|
125
|
+
// A bare IPv6 address like ::1 contains colons that must not be
|
|
126
|
+
// confused with a host:port separator, so we handle it separately.
|
|
127
|
+
let hostname, port
|
|
128
|
+
const ipv6WithPort = entry.match(/^\[(.+)\]:(\d+)$/)
|
|
129
|
+
if (ipv6WithPort) {
|
|
130
|
+
hostname = ipv6WithPort[1]
|
|
131
|
+
port = Number.parseInt(ipv6WithPort[2], 10)
|
|
132
|
+
} else {
|
|
133
|
+
// Bracketed IPv6 without port, or plain hostname[:port], or bare IPv6.
|
|
134
|
+
// Strip optional brackets first.
|
|
135
|
+
const unbracketed = entry.replace(/^\[(.+)\]$/, '$1')
|
|
136
|
+
// A bare IPv6 address contains multiple colons; a hostname:port entry
|
|
137
|
+
// has exactly one colon followed by digits. Only attempt host:port
|
|
138
|
+
// splitting when that is unambiguously the case.
|
|
139
|
+
const colonCount = (unbracketed.match(/:/g) || []).length
|
|
140
|
+
const parsed = colonCount === 1 && unbracketed.match(/^(.+):(\d+)$/)
|
|
141
|
+
hostname = parsed ? parsed[1] : unbracketed
|
|
142
|
+
port = parsed ? Number.parseInt(parsed[2], 10) : 0
|
|
143
|
+
}
|
|
144
|
+
|
|
123
145
|
noProxyEntries.push({
|
|
124
146
|
// strip leading dot or asterisk with dot
|
|
125
|
-
hostname:
|
|
126
|
-
port
|
|
147
|
+
hostname: hostname.replace(/^\*?\./, '').toLowerCase(),
|
|
148
|
+
port
|
|
127
149
|
})
|
|
128
150
|
}
|
|
129
151
|
|
|
@@ -6,7 +6,7 @@ let tls // include tls conditionally since it is not always available
|
|
|
6
6
|
const DispatcherBase = require('./dispatcher-base')
|
|
7
7
|
const { InvalidArgumentError } = require('../core/errors')
|
|
8
8
|
const { Socks5Client, STATES } = require('../core/socks5-client')
|
|
9
|
-
const { kDispatch, kClose, kDestroy } = require('../core/symbols')
|
|
9
|
+
const { kBusy, kConnected, kDispatch, kClose, kDestroy } = require('../core/symbols')
|
|
10
10
|
const Pool = require('./pool')
|
|
11
11
|
const buildConnector = require('../core/connect')
|
|
12
12
|
const { debuglog } = require('node:util')
|
|
@@ -226,6 +226,20 @@ class Socks5ProxyAgent extends DispatcherBase {
|
|
|
226
226
|
}
|
|
227
227
|
})
|
|
228
228
|
this[kPools].set(originKey, pool)
|
|
229
|
+
|
|
230
|
+
const closePoolIfUnused = () => {
|
|
231
|
+
if (this[kPools].get(originKey) !== pool || pool[kConnected] > 0 || pool[kBusy]) {
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this[kPools].delete(originKey)
|
|
236
|
+
if (!pool.destroyed) {
|
|
237
|
+
pool.close()
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
pool.on('disconnect', closePoolIfUnused)
|
|
242
|
+
pool.on('connectionError', closePoolIfUnused)
|
|
229
243
|
}
|
|
230
244
|
|
|
231
245
|
// Dispatch the request through the per-origin pool
|
|
@@ -241,6 +241,11 @@ class RetryHandler {
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
onResponseStart (controller, statusCode, headers, statusMessage) {
|
|
244
|
+
if (statusCode < 200) {
|
|
245
|
+
this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
244
249
|
this.error = null
|
|
245
250
|
this.retryCount += 1
|
|
246
251
|
this.statusCode = statusCode
|
|
@@ -305,7 +310,7 @@ class RetryHandler {
|
|
|
305
310
|
// First time we receive 206
|
|
306
311
|
const range = parseRangeHeader(headers['content-range'])
|
|
307
312
|
|
|
308
|
-
if (range == null) {
|
|
313
|
+
if (range == null || range.end == null) {
|
|
309
314
|
this.headersSent = true
|
|
310
315
|
this.handler.onResponseStart?.(
|
|
311
316
|
this.controllerProxy,
|
|
@@ -330,7 +335,7 @@ class RetryHandler {
|
|
|
330
335
|
}
|
|
331
336
|
|
|
332
337
|
// We make our best to checkpoint the body for further range headers
|
|
333
|
-
if (this.end == null) {
|
|
338
|
+
if (this.end == null && this.opts.method !== 'HEAD') {
|
|
334
339
|
const contentLength = headers['content-length']
|
|
335
340
|
this.end = contentLength != null ? Number(contentLength) - 1 : null
|
|
336
341
|
}
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -540,13 +540,16 @@ module.exports = (opts = {}) => {
|
|
|
540
540
|
|
|
541
541
|
return dispatch => {
|
|
542
542
|
return (opts, handler) => {
|
|
543
|
-
if (
|
|
544
|
-
// Not a method we want to cache
|
|
543
|
+
if (arrayIncludes(safeMethodsToNotCache, opts.method)) {
|
|
544
|
+
// Not a method we want to cache, skip
|
|
545
545
|
return dispatch(opts, handler)
|
|
546
546
|
}
|
|
547
547
|
|
|
548
548
|
// Check if origin is in whitelist
|
|
549
549
|
if (origins !== undefined) {
|
|
550
|
+
if (!opts.origin) {
|
|
551
|
+
return dispatch(opts, handler)
|
|
552
|
+
}
|
|
550
553
|
const requestOrigin = opts.origin.toString().toLowerCase()
|
|
551
554
|
let isAllowed = false
|
|
552
555
|
|
|
@@ -59,7 +59,7 @@ module.exports = (opts = {}) => {
|
|
|
59
59
|
|
|
60
60
|
return dispatch => {
|
|
61
61
|
return (opts, handler) => {
|
|
62
|
-
if (
|
|
62
|
+
if (opts.upgrade || methods.includes(opts.method) === false) {
|
|
63
63
|
return dispatch(opts, handler)
|
|
64
64
|
}
|
|
65
65
|
|
package/lib/mock/mock-utils.js
CHANGED
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
}
|
|
18
18
|
} = require('node:util')
|
|
19
19
|
const { InvalidArgumentError } = require('../core/errors')
|
|
20
|
+
const requestAborted = Symbol('request aborted')
|
|
20
21
|
|
|
21
22
|
function matchValue (match, value) {
|
|
22
23
|
if (typeof match === 'string') {
|
|
@@ -153,6 +154,11 @@ function getResponseData (data) {
|
|
|
153
154
|
return data
|
|
154
155
|
} else if (data instanceof ArrayBuffer) {
|
|
155
156
|
return data
|
|
157
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
158
|
+
// A DataView, or any non-Uint8Array typed array, is a byte container
|
|
159
|
+
// rather than a plain object. Buffer.from() cannot read one directly, so
|
|
160
|
+
// expose the bytes it covers instead of letting it reach JSON.stringify.
|
|
161
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
156
162
|
} else if (typeof data === 'object') {
|
|
157
163
|
return JSON.stringify(data)
|
|
158
164
|
} else if (data) {
|
|
@@ -225,9 +231,15 @@ function deleteMockDispatch (mockDispatches, key) {
|
|
|
225
231
|
}
|
|
226
232
|
|
|
227
233
|
/**
|
|
228
|
-
* @param {string} path Path to remove trailing slash from
|
|
234
|
+
* @param {string|RegExp|Function} path Path, or path matcher, to remove trailing slash from
|
|
229
235
|
*/
|
|
230
236
|
function removeTrailingSlash (path) {
|
|
237
|
+
// Registered path matchers may be a RegExp or a function, which have no
|
|
238
|
+
// trailing slash to strip; hand those back for matchValue to apply.
|
|
239
|
+
if (typeof path !== 'string') {
|
|
240
|
+
return path
|
|
241
|
+
}
|
|
242
|
+
|
|
231
243
|
while (path.endsWith('/')) {
|
|
232
244
|
path = path.slice(0, -1)
|
|
233
245
|
}
|
|
@@ -302,9 +314,13 @@ function mockDispatch (opts, handler) {
|
|
|
302
314
|
mockDispatch.consumed = !mockDispatch.persist && timesInvoked >= times
|
|
303
315
|
mockDispatch.pending = timesInvoked < times
|
|
304
316
|
|
|
317
|
+
const hasBodyHooks = typeof handler.onBodySent === 'function' ||
|
|
318
|
+
typeof handler.onRequestSent === 'function'
|
|
319
|
+
|
|
305
320
|
// Here's where we resolve a callback if a callback is present for the dispatch data.
|
|
306
|
-
if (mockDispatch.data.callback) {
|
|
307
|
-
const
|
|
321
|
+
if (mockDispatch.data.callback && (!hasBodyHooks || opts.body == null)) {
|
|
322
|
+
const { callback, ...responseDefaults } = mockDispatch.data
|
|
323
|
+
const callbackResult = callback(opts)
|
|
308
324
|
|
|
309
325
|
// An asynchronous reply options callback resolves to the reply data, so
|
|
310
326
|
// the dispatch can only continue once the returned promise settles.
|
|
@@ -313,18 +329,25 @@ function mockDispatch (opts, handler) {
|
|
|
313
329
|
if (isPromise(callbackResult)) {
|
|
314
330
|
callbackResult.then(
|
|
315
331
|
(resolvedData) => {
|
|
316
|
-
|
|
332
|
+
if (resolvedData == null || typeof resolvedData !== 'object') {
|
|
333
|
+
handler.onResponseError(null, new InvalidArgumentError('reply options callback must return an object'))
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
mockDispatch.data = { ...responseDefaults, ...resolvedData }
|
|
317
337
|
dispatchMockReply(mockDispatches, mockDispatch, key, opts, handler)
|
|
318
338
|
},
|
|
319
339
|
(error) => {
|
|
320
|
-
deleteMockDispatch(mockDispatches, key)
|
|
321
340
|
handler.onResponseError(null, error)
|
|
322
341
|
}
|
|
323
342
|
)
|
|
324
343
|
return true
|
|
325
344
|
}
|
|
326
345
|
|
|
327
|
-
|
|
346
|
+
if (callbackResult == null || typeof callbackResult !== 'object') {
|
|
347
|
+
throw new InvalidArgumentError('reply options callback must return an object')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
mockDispatch.data = { ...responseDefaults, ...callbackResult }
|
|
328
351
|
}
|
|
329
352
|
|
|
330
353
|
return dispatchMockReply(mockDispatches, mockDispatch, key, opts, handler)
|
|
@@ -335,12 +358,12 @@ function mockDispatch (opts, handler) {
|
|
|
335
358
|
*/
|
|
336
359
|
function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler) {
|
|
337
360
|
// Parse mockDispatch data
|
|
338
|
-
const { data:
|
|
361
|
+
const { data: response, delay } = mockDispatch
|
|
339
362
|
|
|
340
363
|
// If specified, trigger dispatch error
|
|
341
|
-
if (error !== null) {
|
|
364
|
+
if (response.error !== null) {
|
|
342
365
|
deleteMockDispatch(mockDispatches, key)
|
|
343
|
-
handler.onResponseError(null, error)
|
|
366
|
+
handler.onResponseError(null, response.error)
|
|
344
367
|
return true
|
|
345
368
|
}
|
|
346
369
|
|
|
@@ -375,32 +398,107 @@ function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler) {
|
|
|
375
398
|
}
|
|
376
399
|
}
|
|
377
400
|
|
|
401
|
+
let replyOpts = opts
|
|
402
|
+
const dispatches = mockDispatches
|
|
403
|
+
|
|
378
404
|
// Call onRequestStart to allow the handler to receive the controller
|
|
379
405
|
handler.onRequestStart?.(controller, null)
|
|
380
406
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
407
|
+
if (aborted) {
|
|
408
|
+
return true
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const requestBody = dispatchRequestBody(opts.body, handler, controller, () => aborted)
|
|
412
|
+
|
|
413
|
+
if (isPromise(requestBody)) {
|
|
414
|
+
requestBody.then((body) => {
|
|
415
|
+
if (body === requestAborted) {
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (body !== opts.body) {
|
|
420
|
+
replyOpts = { ...opts, body }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
sendReply()
|
|
424
|
+
}, (error) => controller.abort(error))
|
|
425
|
+
return true
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (requestBody === requestAborted) {
|
|
429
|
+
return true
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (requestBody !== opts.body) {
|
|
433
|
+
replyOpts = { ...opts, body: requestBody }
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
sendReply()
|
|
437
|
+
|
|
438
|
+
function sendReply () {
|
|
439
|
+
if (response.callback) {
|
|
440
|
+
const { callback, ...responseDefaults } = response
|
|
441
|
+
let callbackResult
|
|
442
|
+
try {
|
|
443
|
+
callbackResult = callback(replyOpts)
|
|
444
|
+
} catch (err) {
|
|
445
|
+
deleteMockDispatch(mockDispatches, key)
|
|
446
|
+
handler.onResponseError(null, err)
|
|
447
|
+
return
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (isPromise(callbackResult)) {
|
|
451
|
+
callbackResult.then(
|
|
452
|
+
(resolvedData) => {
|
|
453
|
+
if (resolvedData == null || typeof resolvedData !== 'object') {
|
|
454
|
+
handler.onResponseError(null, new InvalidArgumentError('reply options callback must return an object'))
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
mockDispatch.data = { ...responseDefaults, ...resolvedData }
|
|
458
|
+
handleReply(dispatches, mockDispatch.data)
|
|
459
|
+
},
|
|
460
|
+
(err) => {
|
|
461
|
+
handler.onResponseError(null, err)
|
|
462
|
+
}
|
|
463
|
+
)
|
|
464
|
+
return
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (callbackResult == null || typeof callbackResult !== 'object') {
|
|
468
|
+
throw new InvalidArgumentError('reply options callback must return an object')
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
mockDispatch.data = { ...responseDefaults, ...callbackResult }
|
|
472
|
+
handleReply(dispatches, mockDispatch.data)
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Handle the request with a delay if necessary
|
|
477
|
+
if (typeof delay === 'number' && delay > 0) {
|
|
478
|
+
timer = setTimeout(() => {
|
|
479
|
+
timer = null
|
|
480
|
+
handleReply(dispatches)
|
|
481
|
+
}, delay)
|
|
482
|
+
} else {
|
|
483
|
+
handleReply(dispatches)
|
|
484
|
+
}
|
|
389
485
|
}
|
|
390
486
|
|
|
391
|
-
function handleReply (mockDispatches,
|
|
487
|
+
function handleReply (mockDispatches, _response = response) {
|
|
392
488
|
// Don't send response if the request was aborted
|
|
393
489
|
if (aborted) {
|
|
394
490
|
return
|
|
395
491
|
}
|
|
396
492
|
|
|
493
|
+
const { statusCode, data, headers, trailers } = _response
|
|
494
|
+
|
|
397
495
|
// fetch's HeadersList is a 1D string array
|
|
398
496
|
const optsHeaders = Array.isArray(opts.headers)
|
|
399
497
|
? buildHeadersFromArray(opts.headers)
|
|
400
498
|
: opts.headers
|
|
401
|
-
const body = typeof
|
|
402
|
-
?
|
|
403
|
-
:
|
|
499
|
+
const body = typeof data === 'function'
|
|
500
|
+
? data({ ...replyOpts, headers: optsHeaders })
|
|
501
|
+
: data
|
|
404
502
|
|
|
405
503
|
// util.types.isPromise is likely needed for jest.
|
|
406
504
|
if (isPromise(body)) {
|
|
@@ -409,7 +507,7 @@ function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler) {
|
|
|
409
507
|
// synchronously throw the error, which breaks some tests.
|
|
410
508
|
// Rather, we wait for the callback to resolve if it is a
|
|
411
509
|
// promise, and then re-run handleReply with the new body.
|
|
412
|
-
return body.then((newData) => handleReply(mockDispatches, newData))
|
|
510
|
+
return body.then((newData) => handleReply(mockDispatches, { ..._response, data: newData }))
|
|
413
511
|
}
|
|
414
512
|
|
|
415
513
|
// Check again if aborted after async body resolution
|
|
@@ -418,8 +516,8 @@ function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler) {
|
|
|
418
516
|
}
|
|
419
517
|
|
|
420
518
|
const responseData = getResponseData(body)
|
|
421
|
-
const responseHeaders = generateKeyValues(headers)
|
|
422
|
-
const responseTrailers = generateKeyValues(trailers)
|
|
519
|
+
const responseHeaders = generateKeyValues(headers ?? {})
|
|
520
|
+
const responseTrailers = generateKeyValues(trailers ?? {})
|
|
423
521
|
|
|
424
522
|
// Update the controller with response data
|
|
425
523
|
controller.rawHeaders = responseHeaders
|
|
@@ -434,6 +532,97 @@ function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler) {
|
|
|
434
532
|
return true
|
|
435
533
|
}
|
|
436
534
|
|
|
535
|
+
function dispatchRequestBody (body, handler, controller, isAborted) {
|
|
536
|
+
if (typeof handler.onBodySent !== 'function' && typeof handler.onRequestSent !== 'function') {
|
|
537
|
+
return body
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (body == null) {
|
|
541
|
+
return callOnRequestSent(handler, controller, isAborted) ? body : requestAborted
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (body && typeof body[Symbol.asyncIterator] === 'function') {
|
|
545
|
+
return dispatchAsyncIterableBody(body, handler, controller, isAborted)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (isIterableBody(body)) {
|
|
549
|
+
const chunks = []
|
|
550
|
+
|
|
551
|
+
for (const chunk of body) {
|
|
552
|
+
if (isAborted()) {
|
|
553
|
+
return requestAborted
|
|
554
|
+
}
|
|
555
|
+
chunks.push(chunk)
|
|
556
|
+
if (!callOnBodySent(handler, controller, chunk) || isAborted()) {
|
|
557
|
+
return requestAborted
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return callOnRequestSent(handler, controller, isAborted) ? chunks : requestAborted
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (isAborted()) {
|
|
565
|
+
return requestAborted
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (!callOnBodySent(handler, controller, body)) {
|
|
569
|
+
return requestAborted
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return callOnRequestSent(handler, controller, isAborted) ? body : requestAborted
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function dispatchAsyncIterableBody (body, handler, controller, isAborted) {
|
|
576
|
+
const chunks = []
|
|
577
|
+
|
|
578
|
+
for await (const chunk of body) {
|
|
579
|
+
if (isAborted()) {
|
|
580
|
+
return requestAborted
|
|
581
|
+
}
|
|
582
|
+
chunks.push(chunk)
|
|
583
|
+
if (!callOnBodySent(handler, controller, chunk) || isAborted()) {
|
|
584
|
+
return requestAborted
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (!callOnRequestSent(handler, controller, isAborted)) {
|
|
589
|
+
return requestAborted
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
async * [Symbol.asyncIterator] () {
|
|
594
|
+
yield * chunks
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function callOnBodySent (handler, controller, chunk) {
|
|
600
|
+
try {
|
|
601
|
+
handler.onBodySent?.(chunk)
|
|
602
|
+
return true
|
|
603
|
+
} catch (error) {
|
|
604
|
+
controller.abort(error)
|
|
605
|
+
return false
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function callOnRequestSent (handler, controller, isAborted) {
|
|
610
|
+
try {
|
|
611
|
+
handler.onRequestSent?.()
|
|
612
|
+
return !isAborted()
|
|
613
|
+
} catch (error) {
|
|
614
|
+
controller.abort(error)
|
|
615
|
+
return false
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function isIterableBody (body) {
|
|
620
|
+
return typeof body !== 'string' &&
|
|
621
|
+
!Buffer.isBuffer(body) &&
|
|
622
|
+
!ArrayBuffer.isView(body) &&
|
|
623
|
+
typeof body[Symbol.iterator] === 'function'
|
|
624
|
+
}
|
|
625
|
+
|
|
437
626
|
function buildMockDispatch () {
|
|
438
627
|
const agent = this[kMockAgent]
|
|
439
628
|
const origin = this[kOrigin]
|
package/lib/util/cache.js
CHANGED
|
@@ -148,9 +148,7 @@ function getMalformedRestrictiveDirectiveName (key) {
|
|
|
148
148
|
* @param {import('../../types/dispatcher.d.ts').default.DispatchOptions} opts
|
|
149
149
|
*/
|
|
150
150
|
function makeCacheKey (opts) {
|
|
151
|
-
|
|
152
|
-
throw new Error('opts.origin is undefined')
|
|
153
|
-
}
|
|
151
|
+
const origin = opts.origin ? opts.origin.toString() : ''
|
|
154
152
|
|
|
155
153
|
let fullPath = opts.path || '/'
|
|
156
154
|
|
|
@@ -159,7 +157,7 @@ function makeCacheKey (opts) {
|
|
|
159
157
|
}
|
|
160
158
|
|
|
161
159
|
return {
|
|
162
|
-
origin
|
|
160
|
+
origin,
|
|
163
161
|
method: opts.method,
|
|
164
162
|
path: fullPath,
|
|
165
163
|
headers: opts.headers
|
|
@@ -25,6 +25,9 @@ const { SendQueue } = require('./sender')
|
|
|
25
25
|
const { WebsocketFrameSend } = require('./frame')
|
|
26
26
|
const { channels } = require('../../core/diagnostics')
|
|
27
27
|
|
|
28
|
+
const kRef = Symbol.for('nodejs.ref')
|
|
29
|
+
const kUnref = Symbol.for('nodejs.unref')
|
|
30
|
+
|
|
28
31
|
function getSocketAddress (socket) {
|
|
29
32
|
if (typeof socket?.address === 'function') {
|
|
30
33
|
return socket.address()
|
|
@@ -68,6 +71,7 @@ class WebSocket extends EventTarget {
|
|
|
68
71
|
#bufferedAmount = 0
|
|
69
72
|
#protocol = ''
|
|
70
73
|
#extensions = ''
|
|
74
|
+
#refed = true
|
|
71
75
|
|
|
72
76
|
/** @type {SendQueue} */
|
|
73
77
|
#sendQueue
|
|
@@ -194,6 +198,20 @@ class WebSocket extends EventTarget {
|
|
|
194
198
|
this.#binaryType = 'blob'
|
|
195
199
|
}
|
|
196
200
|
|
|
201
|
+
[kRef] () {
|
|
202
|
+
webidl.brandCheck(this, WebSocket)
|
|
203
|
+
|
|
204
|
+
this.#refed = true
|
|
205
|
+
this.#handler.socket?.ref?.()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
[kUnref] () {
|
|
209
|
+
webidl.brandCheck(this, WebSocket)
|
|
210
|
+
|
|
211
|
+
this.#refed = false
|
|
212
|
+
this.#handler.socket?.unref?.()
|
|
213
|
+
}
|
|
214
|
+
|
|
197
215
|
/**
|
|
198
216
|
* @see https://websockets.spec.whatwg.org/#dom-websocket-close
|
|
199
217
|
* @param {number|undefined} code
|
|
@@ -468,6 +486,10 @@ class WebSocket extends EventTarget {
|
|
|
468
486
|
// once this happens, the connection is open
|
|
469
487
|
this.#handler.socket = response.socket
|
|
470
488
|
|
|
489
|
+
if (!this.#refed) {
|
|
490
|
+
this.#handler.socket.unref?.()
|
|
491
|
+
}
|
|
492
|
+
|
|
471
493
|
// Get options from dispatcher options
|
|
472
494
|
const maxFragments = this.#handler.controller.dispatcher?.webSocketOptions?.maxFragments
|
|
473
495
|
const maxPayloadSize = this.#handler.controller.dispatcher?.webSocketOptions?.maxPayloadSize
|
package/package.json
CHANGED
package/types/client.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { URL } from 'node:url'
|
|
2
|
+
import { SessionOptions } from 'node:http2'
|
|
2
3
|
import Dispatcher from './dispatcher'
|
|
3
4
|
import buildConnector from './connector'
|
|
4
5
|
import TClientStats from './client-stats'
|
|
5
6
|
|
|
6
7
|
type ClientConnectOptions<TOpaque = null> = Omit<Dispatcher.ConnectOptions<TOpaque>, 'origin'>
|
|
7
8
|
|
|
9
|
+
// TODO: Pendings
|
|
10
|
+
// 1. Reflect this on Client instantiation
|
|
11
|
+
// 2. Client H2 should use this namespaced options instead.
|
|
12
|
+
|
|
8
13
|
/**
|
|
9
14
|
* A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled by default.
|
|
10
15
|
*/
|
|
@@ -87,23 +92,31 @@ export declare namespace Client {
|
|
|
87
92
|
/**
|
|
88
93
|
* @description Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame.
|
|
89
94
|
* @default 100
|
|
95
|
+
* @deprecated Use h2Options.maxConcurrentStreams instead
|
|
90
96
|
*/
|
|
91
97
|
maxConcurrentStreams?: number;
|
|
92
98
|
/**
|
|
93
99
|
* @description Sets the HTTP/2 stream-level flow-control window size (SETTINGS_INITIAL_WINDOW_SIZE).
|
|
94
100
|
* @default 262144
|
|
101
|
+
* @deprecated Use h2Options.settings.initialWindowSize instead
|
|
95
102
|
*/
|
|
96
103
|
initialWindowSize?: number;
|
|
97
104
|
/**
|
|
98
105
|
* @description Sets the HTTP/2 connection-level flow-control window size (ClientHttp2Session.setLocalWindowSize).
|
|
99
106
|
* @default 524288
|
|
107
|
+
* @deprecated Use h2Options.connectionWindowSize instead
|
|
100
108
|
*/
|
|
101
109
|
connectionWindowSize?: number;
|
|
102
110
|
/**
|
|
103
111
|
* @description Time interval between PING frames dispatch
|
|
104
112
|
* @default 60000
|
|
113
|
+
* @deprecated Use h2Options.connectionWindowSize instead
|
|
105
114
|
*/
|
|
106
115
|
pingInterval?: number;
|
|
116
|
+
/**
|
|
117
|
+
* @description HTTP/2 configuration options
|
|
118
|
+
*/
|
|
119
|
+
h2Options?: Client.H2Options;
|
|
107
120
|
}
|
|
108
121
|
export interface SocketInfo {
|
|
109
122
|
localAddress?: string
|
|
@@ -129,6 +142,33 @@ export declare namespace Client {
|
|
|
129
142
|
*/
|
|
130
143
|
maxPayloadSize?: number;
|
|
131
144
|
}
|
|
145
|
+
|
|
146
|
+
export interface H2Options extends Omit<SessionOptions, keyof buildConnector.BuildOptions> {
|
|
147
|
+
/**
|
|
148
|
+
* @description Sets the HTTP/2 connection-level flow-control window size (ClientHttp2Session.setLocalWindowSize).
|
|
149
|
+
* @default 524288
|
|
150
|
+
*/
|
|
151
|
+
connectionWindowSize?: number;
|
|
152
|
+
/**
|
|
153
|
+
* @description Time interval between PING frames dispatch
|
|
154
|
+
* @default 60000
|
|
155
|
+
*/
|
|
156
|
+
pingInterval?: number;
|
|
157
|
+
/**
|
|
158
|
+
* @description Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame.
|
|
159
|
+
* @default 100
|
|
160
|
+
*/
|
|
161
|
+
maxConcurrentStreams?: number;
|
|
162
|
+
/**
|
|
163
|
+
* @description Enable support for H2C (plain text)
|
|
164
|
+
* @default false
|
|
165
|
+
*/
|
|
166
|
+
useH2c?: boolean;
|
|
167
|
+
/**
|
|
168
|
+
* @description SETTINGS frame object. Default to 'node:http2' defaults
|
|
169
|
+
*/
|
|
170
|
+
settings?: Omit<SessionOptions['settings'], 'enablePush' | 'maxConcurrentStreams' | 'enableConnectProtocol'>
|
|
171
|
+
}
|
|
132
172
|
}
|
|
133
173
|
|
|
134
174
|
export default Client
|