@openclaw/discord 2026.6.33 → 2026.6.35
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/dist/action-runtime-api.js +1 -1
- package/dist/{api-CjnGBhM3.js → api-1GXlSrYP.js} +42 -21
- package/dist/api.js +14 -14
- package/dist/{approval-handler.runtime-BWPQrzpP.js → approval-handler.runtime-Dv4p7REU.js} +3 -3
- package/dist/{audit-BwJ_2OjL.js → audit-DEVi9nIs.js} +3 -3
- package/dist/{channel-Ddnonoy4.js → channel-CQIRiosn.js} +15 -15
- package/dist/{channel-actions-CwmIqnYN.js → channel-actions-JziPEcBU.js} +2 -2
- package/dist/{channel-actions.runtime-DvpTfaJ4.js → channel-actions.runtime-D7L47LY3.js} +5 -5
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel.setup-glZ5Y98a.js → channel.setup-oMeR19Cc.js} +2 -2
- package/dist/{components-j6lTZBsT.js → components-_s5UPJqU.js} +1 -1
- package/dist/{conversation-identity-36FFBwm5.js → conversation-identity-B0k94-bz.js} +2 -2
- package/dist/{directory-config-BYMFzL29.js → directory-config-CD1svkyN.js} +1 -1
- package/dist/directory-contract-api.js +1 -1
- package/dist/{directory-live-9JtJCiu8.js → directory-live-DBTsSRxl.js} +5 -5
- package/dist/{handle-action.guild-admin-xxGYrp-I.js → handle-action.guild-admin-CYIFMxh-.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{manager.runtime-BK6cf2Zu.js → manager.runtime-BZVdGX2d.js} +17 -9
- package/dist/{message-handler-BqpJIavO.js → message-handler-DZd9Bpq8.js} +7 -6
- package/dist/{message-handler.preflight-C90MxCcv.js → message-handler.preflight-Axv9J9EO.js} +12 -11
- package/dist/{message-handler.process-CeQ7Bd_3.js → message-handler.process-CG0zFS-Z.js} +10 -10
- package/dist/{message-utils-CJeAoISH.js → message-utils-Dbuw27gd.js} +1 -1
- package/dist/{outbound-adapter-Dxs6iKEq.js → outbound-adapter-CKC_kmUN.js} +6 -6
- package/dist/pluralkit-CfTReLl3.js +40 -0
- package/dist/{probe-esE_Oqdm.js → probe-DcgZIpyF.js} +3 -2
- package/dist/{probe.runtime-C3Atgpvr.js → probe.runtime-CVwM9L5E.js} +1 -1
- package/dist/{provider-C7376XFH.js → provider-CwwSXIIj.js} +102 -37
- package/dist/{provider-session.runtime-CCdqogGf.js → provider-session.runtime-reBWkqHZ.js} +3 -3
- package/dist/provider.runtime-BsmTLYS_.js +2 -0
- package/dist/{resolve-allowlist-common-3gB2Gpmm.js → resolve-allowlist-common-D7bZqAw3.js} +3 -3
- package/dist/{resolve-channels-B3gQf8QL.js → resolve-channels--oE-RnO6.js} +6 -6
- package/dist/{resolve-users-BO10y5fJ.js → resolve-users-QXo8CXBU.js} +5 -5
- package/dist/{runtime-CFlR3L9X.js → runtime-CF7ramAs.js} +7 -7
- package/dist/runtime-api.actions.js +2 -2
- package/dist/runtime-api.js +19 -19
- package/dist/runtime-api.lookup.js +5 -5
- package/dist/runtime-api.monitor-CzF8MUl_.js +6 -0
- package/dist/runtime-api.monitor.js +5 -5
- package/dist/runtime-api.send.js +5 -5
- package/dist/runtime-api.threads.js +3 -3
- package/dist/{send-JHxfT4Pk.js → send-Bb295iHT.js} +5 -5
- package/dist/{send.components-CN35E2Ck.js → send.components-COkDUO3B.js} +4 -4
- package/dist/{send.outbound-B-cA1Esv.js → send.outbound-B_sk0hNe.js} +3 -3
- package/dist/{send.receipt-CGecJDin.js → send.receipt-Cli6_9Sn.js} +19 -7
- package/dist/{send.shared-vcbfThdy.js → send.shared-BCG7MWnP.js} +2 -2
- package/dist/session-binding-contract-api.js +1 -1
- package/dist/setup-plugin-api.js +1 -1
- package/dist/{subagent-hooks-20wXhVzZ.js → subagent-hooks-CtqoWK4x.js} +2 -2
- package/dist/subagent-hooks-api.js +1 -1
- package/dist/{system-events-DR27TE7p.js → system-events-BxGNk_wE.js} +1 -1
- package/dist/{target-resolver-CDi1gI90.js → target-resolver-BcoK6b0g.js} +2 -2
- package/dist/targets-Q-ZoP7K4.js +3 -0
- package/dist/{thread-bindings-DU4T6nDo.js → thread-bindings-DJ_HT6GJ.js} +4 -4
- package/dist/{thread-bindings.discord-api-CYkr-3Gj.js → thread-bindings.discord-api-OPyjvgyz.js} +4 -4
- package/dist/{thread-bindings.manager-BqWs1UE2.js → thread-bindings.manager-Cd2HAVw_.js} +3 -3
- package/dist/{transcripts-source-D-lBI5ZD.js → transcripts-source-CPf5gURy.js} +1 -1
- package/dist/transcripts-source-api.js +1 -1
- package/dist/{typing-xr05bL-r.js → typing-DY0cwLdg.js} +1 -1
- package/node_modules/undici/README.md +8 -2
- package/node_modules/undici/docs/docs/api/Agent.md +248 -44
- package/node_modules/undici/docs/docs/api/BalancedPool.md +246 -48
- package/node_modules/undici/docs/docs/api/CacheStorage.md +171 -13
- package/node_modules/undici/docs/docs/api/CacheStore.md +294 -98
- package/node_modules/undici/docs/docs/api/Client.md +386 -179
- package/node_modules/undici/docs/docs/api/ClientStats.md +80 -14
- package/node_modules/undici/docs/docs/api/Connector.md +118 -36
- package/node_modules/undici/docs/docs/api/ContentType.md +65 -25
- package/node_modules/undici/docs/docs/api/Cookies.md +122 -71
- package/node_modules/undici/docs/docs/api/Debug.md +34 -18
- package/node_modules/undici/docs/docs/api/DiagnosticsChannel.md +364 -164
- package/node_modules/undici/docs/docs/api/Dispatcher.md +485 -1102
- package/node_modules/undici/docs/docs/api/EnvHttpProxyAgent.md +102 -87
- package/node_modules/undici/docs/docs/api/Errors.md +503 -40
- package/node_modules/undici/docs/docs/api/EventSource.md +298 -32
- package/node_modules/undici/docs/docs/api/Fetch.md +680 -31
- package/node_modules/undici/docs/docs/api/GlobalInstallation.md +78 -98
- package/node_modules/undici/docs/docs/api/H2CClient.md +298 -176
- package/node_modules/undici/docs/docs/api/Interceptors.md +418 -0
- package/node_modules/undici/docs/docs/api/MockAgent.md +333 -314
- package/node_modules/undici/docs/docs/api/MockCallHistory.md +272 -98
- package/node_modules/undici/docs/docs/api/MockCallHistoryLog.md +189 -26
- package/node_modules/undici/docs/docs/api/MockClient.md +151 -33
- package/node_modules/undici/docs/docs/api/MockErrors.md +56 -5
- package/node_modules/undici/docs/docs/api/MockPool.md +321 -258
- package/node_modules/undici/docs/docs/api/Pool.md +235 -43
- package/node_modules/undici/docs/docs/api/PoolStats.md +119 -13
- package/node_modules/undici/docs/docs/api/ProxyAgent.md +179 -160
- package/node_modules/undici/docs/docs/api/RedirectHandler.md +238 -63
- package/node_modules/undici/docs/docs/api/RetryAgent.md +113 -26
- package/node_modules/undici/docs/docs/api/RetryHandler.md +161 -77
- package/node_modules/undici/docs/docs/api/RoundRobinPool.md +166 -72
- package/node_modules/undici/docs/docs/api/SnapshotAgent.md +264 -542
- package/node_modules/undici/docs/docs/api/Socks5ProxyAgent.md +177 -196
- package/node_modules/undici/docs/docs/api/Util.md +68 -11
- package/node_modules/undici/docs/docs/api/WebSocket.md +475 -80
- package/node_modules/undici/docs/docs/api/api-lifecycle.md +100 -32
- package/node_modules/undici/docs/docs/best-practices/client-certificate.md +2 -0
- package/node_modules/undici/docs/docs/best-practices/crawling.md +3 -1
- package/node_modules/undici/docs/docs/best-practices/migrating-from-v7-to-v8.md +6 -4
- package/node_modules/undici/docs/docs/best-practices/mocking-request.md +9 -7
- package/node_modules/undici/docs/docs/best-practices/proxy.md +3 -1
- package/node_modules/undici/docs/docs/best-practices/undici-vs-builtin-fetch.md +10 -8
- package/node_modules/undici/docs/docs/best-practices/writing-tests.md +2 -0
- package/node_modules/undici/docs/docs/{GettingStarted.md → getting-started.md} +22 -19
- package/node_modules/undici/docs/docs/index.md +784 -0
- package/node_modules/undici/docs/docs/site.json +125 -0
- package/node_modules/undici/docs/docs/type-map.json +79 -0
- package/node_modules/undici/lib/api/api-request.js +7 -1
- package/node_modules/undici/lib/api/readable.js +42 -7
- package/node_modules/undici/lib/cache/memory-cache-store.js +56 -11
- package/node_modules/undici/lib/cache/sqlite-cache-store.js +7 -1
- package/node_modules/undici/lib/core/connect.js +16 -2
- package/node_modules/undici/lib/core/errors.js +20 -0
- package/node_modules/undici/lib/core/request.js +14 -3
- package/node_modules/undici/lib/core/symbols.js +3 -0
- package/node_modules/undici/lib/core/util.js +30 -2
- package/node_modules/undici/lib/dispatcher/agent.js +10 -5
- package/node_modules/undici/lib/dispatcher/balanced-pool.js +7 -4
- package/node_modules/undici/lib/dispatcher/client-h1.js +74 -9
- package/node_modules/undici/lib/dispatcher/client-h2.js +483 -151
- package/node_modules/undici/lib/dispatcher/client.js +76 -30
- package/node_modules/undici/lib/dispatcher/dispatcher-base.js +13 -1
- package/node_modules/undici/lib/dispatcher/dispatcher.js +15 -1
- package/node_modules/undici/lib/dispatcher/dispatcher1-wrapper.js +3 -0
- package/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js +37 -8
- package/node_modules/undici/lib/dispatcher/proxy-agent.js +58 -7
- package/node_modules/undici/lib/dispatcher/retry-agent.js +3 -0
- package/node_modules/undici/lib/dispatcher/round-robin-pool.js +1 -1
- package/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js +106 -22
- package/node_modules/undici/lib/global.js +36 -16
- package/node_modules/undici/lib/handler/cache-handler.js +297 -65
- package/node_modules/undici/lib/handler/cache-revalidation-handler.js +13 -3
- package/node_modules/undici/lib/handler/decorator-handler.js +7 -1
- package/node_modules/undici/lib/handler/deduplication-handler.js +12 -6
- package/node_modules/undici/lib/handler/redirect-handler.js +26 -5
- package/node_modules/undici/lib/handler/retry-handler.js +238 -51
- package/node_modules/undici/lib/interceptor/cache.js +221 -58
- package/node_modules/undici/lib/interceptor/decompress.js +124 -15
- package/node_modules/undici/lib/interceptor/deduplicate.js +5 -4
- package/node_modules/undici/lib/interceptor/dns.js +7 -0
- package/node_modules/undici/lib/interceptor/dump.js +10 -23
- package/node_modules/undici/lib/mock/mock-agent.js +13 -1
- package/node_modules/undici/lib/mock/mock-interceptor.js +25 -7
- package/node_modules/undici/lib/mock/mock-utils.js +246 -27
- package/node_modules/undici/lib/util/cache.js +415 -71
- package/node_modules/undici/lib/util/date.js +23 -6
- package/node_modules/undici/lib/web/cookies/parse.js +150 -146
- package/node_modules/undici/lib/web/cookies/util.js +79 -9
- package/node_modules/undici/lib/web/eventsource/eventsource-stream.js +28 -1
- package/node_modules/undici/lib/web/eventsource/eventsource.js +13 -3
- package/node_modules/undici/lib/web/eventsource/util.js +1 -1
- package/node_modules/undici/lib/web/fetch/body.js +2 -1
- package/node_modules/undici/lib/web/fetch/constants.js +1 -1
- package/node_modules/undici/lib/web/fetch/index.js +17 -2
- package/node_modules/undici/lib/web/fetch/request.js +2 -3
- package/node_modules/undici/lib/web/fetch/response.js +1 -3
- package/node_modules/undici/lib/web/fetch/util.js +10 -2
- package/node_modules/undici/lib/web/webidl/index.js +3 -3
- package/node_modules/undici/lib/web/websocket/connection.js +1 -1
- package/node_modules/undici/lib/web/websocket/permessage-deflate.js +5 -0
- package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +8 -10
- package/node_modules/undici/lib/web/websocket/websocket.js +22 -0
- package/node_modules/undici/package.json +4 -4
- package/node_modules/undici/types/client.d.ts +53 -3
- package/node_modules/undici/types/cookies.d.ts +1 -1
- package/node_modules/undici/types/dispatcher.d.ts +3 -3
- package/node_modules/undici/types/errors.d.ts +10 -0
- package/node_modules/undici/types/handlers.d.ts +2 -0
- package/node_modules/undici/types/interceptors.d.ts +2 -0
- package/node_modules/undici/types/mock-agent.d.ts +11 -10
- package/node_modules/undici/types/mock-interceptor.d.ts +5 -1
- package/node_modules/undici/types/proxy-agent.d.ts +7 -0
- package/node_modules/undici/types/socks5-proxy-agent.d.ts +2 -0
- package/npm-shrinkwrap.json +7 -7
- package/package.json +6 -5
- package/dist/pluralkit-DI6RYaVj.js +0 -24
- package/dist/provider.runtime-D1m66kDS.js +0 -2
- package/dist/runtime-api.monitor-4oc58TSk.js +0 -6
- package/dist/targets-C6yd3y8R.js +0 -3
|
@@ -10,7 +10,8 @@ const {
|
|
|
10
10
|
InformationalError,
|
|
11
11
|
InvalidArgumentError,
|
|
12
12
|
HeadersTimeoutError,
|
|
13
|
-
BodyTimeoutError
|
|
13
|
+
BodyTimeoutError,
|
|
14
|
+
ResponseExceededMaxSizeError
|
|
14
15
|
} = require('../core/errors.js')
|
|
15
16
|
const {
|
|
16
17
|
kUrl,
|
|
@@ -26,10 +27,7 @@ const {
|
|
|
26
27
|
kStrictContentLength,
|
|
27
28
|
kOnError,
|
|
28
29
|
kMaxConcurrentStreams,
|
|
29
|
-
kPingInterval,
|
|
30
30
|
kHTTP2Session,
|
|
31
|
-
kHTTP2InitialWindowSize,
|
|
32
|
-
kHTTP2ConnectionWindowSize,
|
|
33
31
|
kHostAuthority,
|
|
34
32
|
kResume,
|
|
35
33
|
kSize,
|
|
@@ -41,7 +39,9 @@ const {
|
|
|
41
39
|
kEnableConnectProtocol,
|
|
42
40
|
kRemoteSettings,
|
|
43
41
|
kHTTP2Stream,
|
|
44
|
-
kHTTP2SessionState
|
|
42
|
+
kHTTP2SessionState,
|
|
43
|
+
kHTTP2Options,
|
|
44
|
+
kMaxResponseSize
|
|
45
45
|
} = require('../core/symbols.js')
|
|
46
46
|
const { channels } = require('../core/diagnostics.js')
|
|
47
47
|
|
|
@@ -51,6 +51,14 @@ const kRequestStream = Symbol('request stream')
|
|
|
51
51
|
const kRequestStreamCleanup = Symbol('request stream cleanup')
|
|
52
52
|
const kRequestStreamState = Symbol('request stream state')
|
|
53
53
|
const kReceivedGoAway = Symbol('received goaway')
|
|
54
|
+
const kGoAwayReplayAttempts = Symbol('goaway replay attempts')
|
|
55
|
+
const kRefusedStreamRetry = Symbol('refused stream retry')
|
|
56
|
+
|
|
57
|
+
// RFC 9113 section 8.7: a client SHOULD NOT automatically retry a request more
|
|
58
|
+
// than once. Without a budget a peer that keeps refusing turns one request into
|
|
59
|
+
// an unbounded connect/refuse/reconnect loop that never settles and starves the
|
|
60
|
+
// event loop.
|
|
61
|
+
const MAX_GOAWAY_REPLAY_ATTEMPTS = 1
|
|
54
62
|
|
|
55
63
|
let extractBody
|
|
56
64
|
|
|
@@ -154,13 +162,24 @@ function requeueUnsentRequest (client, request) {
|
|
|
154
162
|
}
|
|
155
163
|
|
|
156
164
|
function completeRequest (client, request, resetPendingIdx = false) {
|
|
157
|
-
const
|
|
165
|
+
const queue = client[kQueue]
|
|
166
|
+
const runningIdx = client[kRunningIdx]
|
|
167
|
+
|
|
168
|
+
// In-order completion: clear the request and advance without splicing.
|
|
169
|
+
// The client's resume loop compacts cleared slots once the index grows.
|
|
170
|
+
if (runningIdx < client[kPendingIdx] && queue[runningIdx] === request) {
|
|
171
|
+
queue[runningIdx] = null
|
|
172
|
+
client[kRunningIdx] = runningIdx + 1
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const index = queue.indexOf(request, runningIdx)
|
|
158
177
|
|
|
159
178
|
if (index === -1 || index >= client[kPendingIdx]) {
|
|
160
179
|
return
|
|
161
180
|
}
|
|
162
181
|
|
|
163
|
-
|
|
182
|
+
queue.splice(index, 1)
|
|
164
183
|
client[kPendingIdx]--
|
|
165
184
|
|
|
166
185
|
if (resetPendingIdx && client[kPendingIdx] < client[kRunningIdx]) {
|
|
@@ -168,17 +187,25 @@ function completeRequest (client, request, resetPendingIdx = false) {
|
|
|
168
187
|
}
|
|
169
188
|
}
|
|
170
189
|
|
|
171
|
-
function
|
|
190
|
+
function canReplayRequest (request) {
|
|
172
191
|
const { body } = request
|
|
173
192
|
|
|
174
193
|
return body == null || util.isBuffer(body) || util.isBlobLike(body)
|
|
175
194
|
}
|
|
176
195
|
|
|
177
|
-
|
|
178
|
-
|
|
196
|
+
// Count a GOAWAY refusal against the request's replay budget. A peer that
|
|
197
|
+
// refuses every connection must eventually surface an error to the caller
|
|
198
|
+
// rather than being retried forever. Kept separate from canReplayRequest so
|
|
199
|
+
// that the REFUSED_STREAM retry, which has its own single-attempt limit, does
|
|
200
|
+
// not consume this budget just by asking whether the body can be replayed.
|
|
201
|
+
function registerGoAwayRefusal (request) {
|
|
202
|
+
const attempts = (request[kGoAwayReplayAttempts] ?? 0) + 1
|
|
203
|
+
request[kGoAwayReplayAttempts] = attempts
|
|
179
204
|
|
|
180
|
-
|
|
205
|
+
return attempts <= MAX_GOAWAY_REPLAY_ATTEMPTS
|
|
206
|
+
}
|
|
181
207
|
|
|
208
|
+
function closeStream (stream, code = NGHTTP2_REFUSED_STREAM) {
|
|
182
209
|
if (stream != null && !stream.destroyed && !stream.closed) {
|
|
183
210
|
try {
|
|
184
211
|
stream.close(code)
|
|
@@ -186,15 +213,48 @@ function closeRequestStream (request, code = NGHTTP2_REFUSED_STREAM) {
|
|
|
186
213
|
}
|
|
187
214
|
}
|
|
188
215
|
|
|
216
|
+
function detachRequestStreamForClose (request) {
|
|
217
|
+
const stream = request[kRequestStream]
|
|
218
|
+
|
|
219
|
+
clearRequestStream(request)
|
|
220
|
+
severRequestStream(stream)
|
|
221
|
+
|
|
222
|
+
return stream
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Unbind a stream from its request for good. releaseRequestStream() alone
|
|
226
|
+
// leaves the 'close' listener attached and kRequestStreamState populated, so a
|
|
227
|
+
// stream abandoned here would still run completeRequestStream() later — and
|
|
228
|
+
// splice out the request that has since been requeued onto another session.
|
|
229
|
+
function severRequestStream (stream) {
|
|
230
|
+
if (stream == null || stream[kRequestStreamState] == null) {
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
stream[kRequestStreamState] = null
|
|
235
|
+
stream.off('close', completeRequestStream)
|
|
236
|
+
// Upgrade streams use their own close cleanup, which would otherwise release
|
|
237
|
+
// the session a second time after the stream has been severed for GOAWAY.
|
|
238
|
+
stream.off('close', onUpgradeStreamClose)
|
|
239
|
+
|
|
240
|
+
if (stream[kHTTP2Session] != null) {
|
|
241
|
+
closeStreamSession(stream)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!stream.destroyed && !stream.closed) {
|
|
245
|
+
stream.once('error', noop)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
189
249
|
function connectH2 (client, socket) {
|
|
190
250
|
client[kSocket] = socket
|
|
191
251
|
|
|
192
|
-
const http2InitialWindowSize = client[
|
|
193
|
-
const http2ConnectionWindowSize = client[
|
|
252
|
+
const http2InitialWindowSize = client[kHTTP2Options].sessionOptions?.initialWindowSize
|
|
253
|
+
const http2ConnectionWindowSize = client[kHTTP2Options].connectionWindowSize
|
|
194
254
|
|
|
195
255
|
const session = http2.connect(client[kUrl], {
|
|
196
256
|
createConnection: () => socket,
|
|
197
|
-
peerMaxConcurrentStreams: client[
|
|
257
|
+
peerMaxConcurrentStreams: client[kHTTP2Options].maxConcurrentStreams,
|
|
198
258
|
settings: {
|
|
199
259
|
// TODO(metcoder95): add support for PUSH
|
|
200
260
|
enablePush: false,
|
|
@@ -208,8 +268,16 @@ function connectH2 (client, socket) {
|
|
|
208
268
|
session[kSocket] = socket
|
|
209
269
|
session[kHTTP2SessionState] = {
|
|
210
270
|
idleTimeout: null,
|
|
271
|
+
// Armed while the peer advertises MAX_CONCURRENT_STREAMS = 0 and we have
|
|
272
|
+
// work that cannot start. See setNoStreamsTimeout.
|
|
273
|
+
noStreamsTimeout: null,
|
|
274
|
+
// Sockets start out ref'd. Session ref/unref proxies to the socket, so a
|
|
275
|
+
// single cached flag lets us skip redundant uv ref/unref calls, provided
|
|
276
|
+
// every ref/unref of the session or its socket goes through
|
|
277
|
+
// refH2Session/unrefH2Session.
|
|
278
|
+
refed: true,
|
|
211
279
|
ping: {
|
|
212
|
-
interval: client[
|
|
280
|
+
interval: client[kHTTP2Options].pingInterval === 0 ? null : setInterval(onHttp2SendPing, client[kHTTP2Options].pingInterval, session).unref()
|
|
213
281
|
}
|
|
214
282
|
}
|
|
215
283
|
session[kReceivedGoAway] = false
|
|
@@ -227,14 +295,13 @@ function connectH2 (client, socket) {
|
|
|
227
295
|
|
|
228
296
|
util.addListener(session, 'error', onHttp2SessionError)
|
|
229
297
|
util.addListener(session, 'frameError', onHttp2FrameError)
|
|
230
|
-
util.addListener(session, 'end', onHttp2SessionEnd)
|
|
231
298
|
util.addListener(session, 'goaway', onHttp2SessionGoAway)
|
|
232
299
|
util.addListener(session, 'close', onHttp2SessionClose)
|
|
233
300
|
util.addListener(session, 'remoteSettings', onHttp2RemoteSettings)
|
|
234
301
|
// TODO (@metcoder95): implement SETTINGS support
|
|
235
302
|
// util.addListener(session, 'localSettings', onHttp2RemoteSettings)
|
|
236
303
|
|
|
237
|
-
session
|
|
304
|
+
unrefH2Session(session)
|
|
238
305
|
|
|
239
306
|
client[kHTTP2Session] = session
|
|
240
307
|
socket[kHTTP2Session] = session
|
|
@@ -303,16 +370,6 @@ function connectH2 (client, socket) {
|
|
|
303
370
|
// Don't dispatch an upgrade until all preceding requests have completed.
|
|
304
371
|
// Possibly, we do not have remote settings confirmed yet.
|
|
305
372
|
if ((request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false) return true
|
|
306
|
-
// Request with stream or iterator body can error while other requests
|
|
307
|
-
// are inflight and indirectly error those as well.
|
|
308
|
-
// Ensure this doesn't happen by waiting for inflight
|
|
309
|
-
// to complete before dispatching.
|
|
310
|
-
|
|
311
|
-
// Request with stream or iterator body cannot be retried.
|
|
312
|
-
// Ensure that no other requests are inflight and
|
|
313
|
-
// could cause failure.
|
|
314
|
-
if (util.bodyLength(request.body) !== 0 &&
|
|
315
|
-
(util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) return true
|
|
316
373
|
} else {
|
|
317
374
|
return (request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false
|
|
318
375
|
}
|
|
@@ -323,17 +380,36 @@ function connectH2 (client, socket) {
|
|
|
323
380
|
}
|
|
324
381
|
}
|
|
325
382
|
|
|
383
|
+
// Session ref/unref proxies to the underlying socket, so refH2Session and
|
|
384
|
+
// unrefH2Session cover both and can skip the call when the cached ref state
|
|
385
|
+
// already matches.
|
|
386
|
+
function refH2Session (session) {
|
|
387
|
+
const state = session[kHTTP2SessionState]
|
|
388
|
+
|
|
389
|
+
if (state.refed === false) {
|
|
390
|
+
state.refed = true
|
|
391
|
+
session.ref()
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function unrefH2Session (session) {
|
|
396
|
+
const state = session[kHTTP2SessionState]
|
|
397
|
+
|
|
398
|
+
if (state.refed === true) {
|
|
399
|
+
state.refed = false
|
|
400
|
+
session.unref()
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
326
404
|
function resumeH2 (client) {
|
|
327
405
|
const socket = client[kSocket]
|
|
328
406
|
const session = client[kHTTP2Session]
|
|
329
407
|
|
|
330
408
|
if (socket?.destroyed === false) {
|
|
331
409
|
if (client[kSize] === 0 || client[kMaxConcurrentStreams] === 0) {
|
|
332
|
-
|
|
333
|
-
session.unref()
|
|
410
|
+
unrefH2Session(session)
|
|
334
411
|
} else {
|
|
335
|
-
|
|
336
|
-
session.ref()
|
|
412
|
+
refH2Session(session)
|
|
337
413
|
}
|
|
338
414
|
|
|
339
415
|
if (client[kSize] === 0 && session[kOpenStreams] === 0) {
|
|
@@ -341,7 +417,74 @@ function resumeH2 (client) {
|
|
|
341
417
|
} else {
|
|
342
418
|
clearHttp2IdleTimeout(session)
|
|
343
419
|
}
|
|
420
|
+
|
|
421
|
+
if (client[kMaxConcurrentStreams] === 0 && client[kRunning] === 0 && client[kPending] > 0) {
|
|
422
|
+
setNoStreamsTimeout(session)
|
|
423
|
+
} else {
|
|
424
|
+
clearNoStreamsTimeout(session)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function clearNoStreamsTimeout (session) {
|
|
430
|
+
const state = session[kHTTP2SessionState]
|
|
431
|
+
|
|
432
|
+
if (state?.noStreamsTimeout != null) {
|
|
433
|
+
clearTimeout(state.noStreamsTimeout)
|
|
434
|
+
state.noStreamsTimeout = null
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// A peer is allowed to advertise SETTINGS_MAX_CONCURRENT_STREAMS = 0 to refuse
|
|
439
|
+
// new streams (RFC 9113 §6.5.2), and is expected to raise it again later. Until
|
|
440
|
+
// it does, busy() reports the client as permanently busy and queued requests
|
|
441
|
+
// cannot open a stream — which means no per-stream timeout covers them, and no
|
|
442
|
+
// reconnect can happen either, so the SETTINGS frame that would lift the limit
|
|
443
|
+
// can never arrive. Give the peer headersTimeout to start honouring requests
|
|
444
|
+
// before failing them; a request that cannot even be sent has missed the same
|
|
445
|
+
// deadline as one whose headers never arrive.
|
|
446
|
+
function setNoStreamsTimeout (session) {
|
|
447
|
+
const client = session[kClient]
|
|
448
|
+
const state = session[kHTTP2SessionState]
|
|
449
|
+
const timeout = client[kHeadersTimeout]
|
|
450
|
+
|
|
451
|
+
if (!timeout || state.noStreamsTimeout != null) {
|
|
452
|
+
return
|
|
344
453
|
}
|
|
454
|
+
|
|
455
|
+
state.noStreamsTimeout = setTimeout(onNoStreamsTimeout, timeout, session).unref()
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function onNoStreamsTimeout (session) {
|
|
459
|
+
const client = session[kClient]
|
|
460
|
+
const state = session[kHTTP2SessionState]
|
|
461
|
+
|
|
462
|
+
state.noStreamsTimeout = null
|
|
463
|
+
|
|
464
|
+
if (
|
|
465
|
+
client[kHTTP2Session] !== session ||
|
|
466
|
+
client[kMaxConcurrentStreams] !== 0 ||
|
|
467
|
+
client[kRunning] !== 0 ||
|
|
468
|
+
client[kPending] === 0
|
|
469
|
+
) {
|
|
470
|
+
return
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const err = new HeadersTimeoutError(
|
|
474
|
+
`HTTP/2: server did not accept a new stream within ${client[kHeadersTimeout]}`
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
const requests = client[kQueue].splice(client[kPendingIdx])
|
|
478
|
+
for (let i = 0; i < requests.length; i++) {
|
|
479
|
+
if (requests[i] != null) {
|
|
480
|
+
util.errorRequest(client, requests[i], err)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Drop the unusable session so the next request gets a fresh connection,
|
|
485
|
+
// whose SETTINGS may well allow streams again.
|
|
486
|
+
session[kError] = err
|
|
487
|
+
resetHttp2Session(session, err)
|
|
345
488
|
}
|
|
346
489
|
|
|
347
490
|
function clearHttp2IdleTimeout (session) {
|
|
@@ -447,23 +590,26 @@ function onHttp2SessionError (err) {
|
|
|
447
590
|
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
448
591
|
|
|
449
592
|
this[kSocket][kError] = err
|
|
593
|
+
|
|
594
|
+
if (this[kReceivedGoAway]) {
|
|
595
|
+
return
|
|
596
|
+
}
|
|
597
|
+
|
|
450
598
|
this[kClient][kOnError](err)
|
|
451
599
|
}
|
|
452
600
|
|
|
453
601
|
function onHttp2FrameError (type, code, id) {
|
|
454
602
|
if (id === 0) {
|
|
603
|
+
if (this[kReceivedGoAway]) {
|
|
604
|
+
return
|
|
605
|
+
}
|
|
606
|
+
|
|
455
607
|
const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
|
|
456
608
|
this[kSocket][kError] = err
|
|
457
609
|
this[kClient][kOnError](err)
|
|
458
610
|
}
|
|
459
611
|
}
|
|
460
612
|
|
|
461
|
-
function onHttp2SessionEnd () {
|
|
462
|
-
const err = new SocketError('other side closed', util.getSocketInfo(this[kSocket]))
|
|
463
|
-
this.destroy(err)
|
|
464
|
-
util.destroy(this[kSocket], err)
|
|
465
|
-
}
|
|
466
|
-
|
|
467
613
|
/**
|
|
468
614
|
* This is the root cause of #3011
|
|
469
615
|
* We need to handle GOAWAY frames properly, and trigger the session close
|
|
@@ -485,14 +631,18 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
|
|
|
485
631
|
const previousPendingIdx = client[kPendingIdx]
|
|
486
632
|
const pendingIdx = getGoAwayPendingIdx(client, lastStreamID)
|
|
487
633
|
const retriableRequests = []
|
|
634
|
+
const streamsToClose = []
|
|
488
635
|
|
|
636
|
+
// Closing one stream after GOAWAY can synchronously emit frameError on
|
|
637
|
+
// sibling streams. Detach all affected requests first so those errors do
|
|
638
|
+
// not fail requests that are about to be requeued.
|
|
489
639
|
for (let i = pendingIdx; i < previousPendingIdx; i++) {
|
|
490
640
|
const request = client[kQueue][i]
|
|
491
641
|
|
|
492
642
|
if (request != null) {
|
|
493
|
-
|
|
643
|
+
streamsToClose.push(detachRequestStreamForClose(request))
|
|
494
644
|
|
|
495
|
-
if (
|
|
645
|
+
if (canReplayRequest(request) && registerGoAwayRefusal(request)) {
|
|
496
646
|
retriableRequests.push(request)
|
|
497
647
|
} else {
|
|
498
648
|
util.errorRequest(client, request, err)
|
|
@@ -500,6 +650,10 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
|
|
|
500
650
|
}
|
|
501
651
|
}
|
|
502
652
|
|
|
653
|
+
for (let i = 0; i < streamsToClose.length; i++) {
|
|
654
|
+
closeStream(streamsToClose[i])
|
|
655
|
+
}
|
|
656
|
+
|
|
503
657
|
if (pendingIdx !== previousPendingIdx) {
|
|
504
658
|
const remainingPendingRequests = client[kQueue].slice(previousPendingIdx)
|
|
505
659
|
client[kQueue].length = pendingIdx
|
|
@@ -513,6 +667,7 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
|
|
|
513
667
|
}
|
|
514
668
|
|
|
515
669
|
clearHttp2IdleTimeout(this)
|
|
670
|
+
clearNoStreamsTimeout(this)
|
|
516
671
|
|
|
517
672
|
if (!this.closed && !this.destroyed) {
|
|
518
673
|
this.close()
|
|
@@ -537,6 +692,7 @@ function onHttp2SessionClose () {
|
|
|
537
692
|
}
|
|
538
693
|
|
|
539
694
|
clearHttp2IdleTimeout(this)
|
|
695
|
+
clearNoStreamsTimeout(this)
|
|
540
696
|
|
|
541
697
|
if (state.ping.interval != null) {
|
|
542
698
|
clearInterval(state.ping.interval)
|
|
@@ -595,7 +751,11 @@ function onHttp2SocketError (err) {
|
|
|
595
751
|
|
|
596
752
|
this[kError] = err
|
|
597
753
|
|
|
598
|
-
this[
|
|
754
|
+
if (this[kHTTP2Session]?.[kReceivedGoAway]) {
|
|
755
|
+
return
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
this[kHTTP2Session]?.[kClient]?.[kOnError](err)
|
|
599
759
|
}
|
|
600
760
|
|
|
601
761
|
function onHttp2SocketEnd () {
|
|
@@ -614,7 +774,7 @@ function closeStreamSession (stream) {
|
|
|
614
774
|
stream[kHTTP2Session] = null
|
|
615
775
|
session[kOpenStreams] -= 1
|
|
616
776
|
if (session[kOpenStreams] === 0) {
|
|
617
|
-
session
|
|
777
|
+
unrefH2Session(session)
|
|
618
778
|
setHttp2IdleTimeout(session)
|
|
619
779
|
}
|
|
620
780
|
}
|
|
@@ -629,22 +789,34 @@ function onUpgradeStreamClose () {
|
|
|
629
789
|
closeStreamSession(this)
|
|
630
790
|
}
|
|
631
791
|
|
|
632
|
-
|
|
792
|
+
// Idempotent terminal cleanup, called from both 'end' and 'close': the
|
|
793
|
+
// null-state guard no-ops the later call.
|
|
794
|
+
function completeRequestStream () {
|
|
633
795
|
const state = this[kRequestStreamState]
|
|
634
796
|
|
|
635
|
-
if (state) {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
releaseRequestStream(this)
|
|
797
|
+
if (state == null) {
|
|
798
|
+
return
|
|
799
|
+
}
|
|
639
800
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
801
|
+
// Release the stream first so request references are cleared,
|
|
802
|
+
// then complete the response with trailers if available.
|
|
803
|
+
releaseRequestStream(this)
|
|
804
|
+
|
|
805
|
+
if (state.pendingEnd && !state.request.aborted && !state.request.completed) {
|
|
806
|
+
state.request.onResponseEnd(state.trailers || {})
|
|
807
|
+
} else if (!state.request.aborted && !state.request.completed) {
|
|
808
|
+
// The stream closed without a complete response and without reporting an
|
|
809
|
+
// error. finalizeRequest() below frees the queue slot either way, so
|
|
810
|
+
// without this the request would simply vanish and its caller would never
|
|
811
|
+
// hear back.
|
|
812
|
+
util.errorRequest(
|
|
813
|
+
state.client,
|
|
814
|
+
state.request,
|
|
815
|
+
new InformationalError('HTTP/2: stream closed before the response was complete')
|
|
816
|
+
)
|
|
644
817
|
}
|
|
645
818
|
|
|
646
|
-
|
|
647
|
-
this.off('error', noop)
|
|
819
|
+
finalizeRequest(state)
|
|
648
820
|
closeStreamSession(this)
|
|
649
821
|
this[kRequestStreamState] = null
|
|
650
822
|
}
|
|
@@ -767,7 +939,7 @@ function onUpgradeResponse (headers, _flags) {
|
|
|
767
939
|
|
|
768
940
|
removeUpgradeStreamListeners(stream)
|
|
769
941
|
detachRequestFromStream(request)
|
|
770
|
-
|
|
942
|
+
finalizeRequest(state)
|
|
771
943
|
}
|
|
772
944
|
|
|
773
945
|
function setupUpgradeStream (stream, state) {
|
|
@@ -790,12 +962,51 @@ function setupUpgradeStream (stream, state) {
|
|
|
790
962
|
stream.setTimeout(headersTimeout)
|
|
791
963
|
}
|
|
792
964
|
|
|
965
|
+
function finalizeRequest (state, resetPendingIdx = false) {
|
|
966
|
+
if (state.requestFinalized) {
|
|
967
|
+
return
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
state.requestFinalized = true
|
|
971
|
+
completeRequest(state.client, state.request, resetPendingIdx)
|
|
972
|
+
|
|
973
|
+
state.client[kResume]()
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function openStream (client, request, session, abort, headers, options) {
|
|
977
|
+
try {
|
|
978
|
+
return session.request(headers, options)
|
|
979
|
+
} catch (err) {
|
|
980
|
+
// A GOAWAY'd session rejects new streams, same as an invalid session:
|
|
981
|
+
// reset and requeue on a fresh connection rather than the destroy + abort
|
|
982
|
+
// below, whose destroy(socket, err) can crash via an unhandled 'error'.
|
|
983
|
+
if (err?.code === 'ERR_HTTP2_INVALID_SESSION' || err?.code === 'ERR_HTTP2_GOAWAY_SESSION') {
|
|
984
|
+
const wrappedErr = new SocketError(err.message, util.getSocketInfo(session[kSocket]))
|
|
985
|
+
wrappedErr.cause = err
|
|
986
|
+
session[kError] = wrappedErr
|
|
987
|
+
resetHttp2Session(session, wrappedErr)
|
|
988
|
+
requeueUnsentRequest(client, request)
|
|
989
|
+
|
|
990
|
+
return null
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const wrappedErr = new InformationalError(err.message, { cause: err })
|
|
994
|
+
session[kError] = wrappedErr
|
|
995
|
+
session[kSocket][kError] = wrappedErr
|
|
996
|
+
|
|
997
|
+
session.destroy(wrappedErr)
|
|
998
|
+
util.destroy(session[kSocket], wrappedErr)
|
|
999
|
+
abort(wrappedErr)
|
|
1000
|
+
|
|
1001
|
+
return null
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
793
1005
|
function writeH2 (client, request) {
|
|
794
1006
|
const headersTimeout = request.headersTimeout ?? client[kHeadersTimeout]
|
|
795
1007
|
const bodyTimeout = request.bodyTimeout ?? client[kBodyTimeout]
|
|
796
1008
|
const session = client[kHTTP2Session]
|
|
797
1009
|
const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
|
|
798
|
-
let { body } = request
|
|
799
1010
|
|
|
800
1011
|
if (upgrade != null && upgrade !== 'websocket') {
|
|
801
1012
|
util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`))
|
|
@@ -804,22 +1015,30 @@ function writeH2 (client, request) {
|
|
|
804
1015
|
|
|
805
1016
|
const headers = buildRequestHeaders(reqHeaders)
|
|
806
1017
|
|
|
807
|
-
/** @type {import('node:http2').ClientHttp2Stream} */
|
|
808
|
-
let stream = null
|
|
809
|
-
|
|
810
1018
|
headers[HTTP2_HEADER_AUTHORITY] = host || client[kHostAuthority]
|
|
811
1019
|
headers[HTTP2_HEADER_METHOD] = method
|
|
812
1020
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1021
|
+
// Single pre-shaped state object shared by all stream event handlers.
|
|
1022
|
+
// All fields are declared up-front so the object keeps a stable hidden
|
|
1023
|
+
// class for the whole request lifetime.
|
|
1024
|
+
const state = {
|
|
1025
|
+
abort: null,
|
|
1026
|
+
body: request.body,
|
|
1027
|
+
bytesRead: 0,
|
|
1028
|
+
client,
|
|
1029
|
+
contentLength: null,
|
|
1030
|
+
expectsPayload: false,
|
|
1031
|
+
maxResponseSize: client[kMaxResponseSize],
|
|
1032
|
+
request,
|
|
1033
|
+
headersTimeout,
|
|
1034
|
+
bodyTimeout,
|
|
1035
|
+
requestFinalized: false,
|
|
1036
|
+
responseReceived: false,
|
|
1037
|
+
bodySent: false,
|
|
1038
|
+
pendingEnd: false,
|
|
1039
|
+
trailers: null,
|
|
1040
|
+
session,
|
|
1041
|
+
stream: null
|
|
823
1042
|
}
|
|
824
1043
|
|
|
825
1044
|
const abort = (err, resetPendingIdx = false) => {
|
|
@@ -831,47 +1050,39 @@ function writeH2 (client, request) {
|
|
|
831
1050
|
|
|
832
1051
|
util.errorRequest(client, request, err)
|
|
833
1052
|
|
|
834
|
-
if (stream != null) {
|
|
1053
|
+
if (state.stream != null) {
|
|
835
1054
|
clearRequestStream(request)
|
|
836
1055
|
|
|
837
|
-
// On Abort, we close the stream to send RST_STREAM frame
|
|
1056
|
+
// On Abort, we close the stream to send RST_STREAM frame.
|
|
1057
|
+
const stream = state.stream
|
|
838
1058
|
stream.close()
|
|
839
1059
|
|
|
1060
|
+
// close() alone leaves cleanup waiting on the 'close' event; on a busy,
|
|
1061
|
+
// long-lived multiplexed session that event can fail to fire, leaving the
|
|
1062
|
+
// native Http2Stream (and the whole request graph it pins) alive for the
|
|
1063
|
+
// session's life. Destroy the stream synchronously to release the handle
|
|
1064
|
+
// deterministically. Deferring the destroy (e.g. via setImmediate) leaks
|
|
1065
|
+
// the same way when the event loop is stalled and the callback never runs
|
|
1066
|
+
// under abort churn (#5558); close() has already queued the RST_STREAM
|
|
1067
|
+
// frame on the native session, so a synchronous destroy still sends it.
|
|
1068
|
+
if (!stream.destroyed) {
|
|
1069
|
+
util.destroy(stream)
|
|
1070
|
+
}
|
|
1071
|
+
|
|
840
1072
|
// We move the running index to the next request
|
|
841
1073
|
client[kOnError](err)
|
|
842
|
-
finalizeRequest(resetPendingIdx)
|
|
1074
|
+
finalizeRequest(state, resetPendingIdx)
|
|
843
1075
|
}
|
|
844
1076
|
|
|
845
1077
|
// We do not destroy the socket as we can continue using the session
|
|
846
1078
|
// the stream gets destroyed and the session remains to create new streams
|
|
847
|
-
util.destroy(body, err)
|
|
1079
|
+
util.destroy(state.body, err)
|
|
848
1080
|
}
|
|
849
1081
|
|
|
850
|
-
|
|
851
|
-
try {
|
|
852
|
-
return session.request(headers, options)
|
|
853
|
-
} catch (err) {
|
|
854
|
-
if (err?.code === 'ERR_HTTP2_INVALID_SESSION') {
|
|
855
|
-
const wrappedErr = new SocketError(err.message, util.getSocketInfo(session[kSocket]))
|
|
856
|
-
wrappedErr.cause = err
|
|
857
|
-
session[kError] = wrappedErr
|
|
858
|
-
resetHttp2Session(session, wrappedErr)
|
|
859
|
-
requeueUnsentRequest(client, request)
|
|
860
|
-
|
|
861
|
-
return null
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
const wrappedErr = new InformationalError(err.message, { cause: err })
|
|
865
|
-
session[kError] = wrappedErr
|
|
866
|
-
session[kSocket][kError] = wrappedErr
|
|
867
|
-
|
|
868
|
-
session.destroy(wrappedErr)
|
|
869
|
-
util.destroy(session[kSocket], wrappedErr)
|
|
870
|
-
abort(wrappedErr)
|
|
1082
|
+
state.abort = abort
|
|
871
1083
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
}
|
|
1084
|
+
/** @type {import('node:http2').ClientHttp2Stream} */
|
|
1085
|
+
let stream = null
|
|
875
1086
|
|
|
876
1087
|
try {
|
|
877
1088
|
// We are already connected, streams are pending.
|
|
@@ -886,24 +1097,13 @@ function writeH2 (client, request) {
|
|
|
886
1097
|
}
|
|
887
1098
|
|
|
888
1099
|
if (upgrade || method === 'CONNECT') {
|
|
889
|
-
session
|
|
890
|
-
|
|
891
|
-
const upgradeState = {
|
|
892
|
-
abort,
|
|
893
|
-
finalizeRequest,
|
|
894
|
-
request,
|
|
895
|
-
headersTimeout,
|
|
896
|
-
bodyTimeout,
|
|
897
|
-
responseReceived: false,
|
|
898
|
-
session,
|
|
899
|
-
stream: null
|
|
900
|
-
}
|
|
1100
|
+
refH2Session(session)
|
|
901
1101
|
|
|
902
1102
|
if (upgrade === 'websocket') {
|
|
903
1103
|
// We cannot upgrade to websocket if extended CONNECT protocol is not supported
|
|
904
1104
|
if (session[kEnableConnectProtocol] === false) {
|
|
905
1105
|
util.errorRequest(client, request, new InformationalError('HTTP/2: Extended CONNECT protocol not supported by server'))
|
|
906
|
-
session
|
|
1106
|
+
unrefH2Session(session)
|
|
907
1107
|
return false
|
|
908
1108
|
}
|
|
909
1109
|
|
|
@@ -921,12 +1121,12 @@ function writeH2 (client, request) {
|
|
|
921
1121
|
headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https'
|
|
922
1122
|
}
|
|
923
1123
|
|
|
924
|
-
stream =
|
|
1124
|
+
stream = openStream(client, request, session, abort, headers, { endStream: false, signal })
|
|
925
1125
|
if (stream == null) {
|
|
926
|
-
session
|
|
1126
|
+
unrefH2Session(session)
|
|
927
1127
|
return false
|
|
928
1128
|
}
|
|
929
|
-
setupUpgradeStream(stream,
|
|
1129
|
+
setupUpgradeStream(stream, state)
|
|
930
1130
|
return true
|
|
931
1131
|
}
|
|
932
1132
|
|
|
@@ -935,12 +1135,12 @@ function writeH2 (client, request) {
|
|
|
935
1135
|
// will create a new stream. We trigger a request to create the stream and wait until
|
|
936
1136
|
// `ready` event is triggered
|
|
937
1137
|
// We disabled endStream to allow the user to write to the stream
|
|
938
|
-
stream =
|
|
1138
|
+
stream = openStream(client, request, session, abort, headers, { endStream: false, signal })
|
|
939
1139
|
if (stream == null) {
|
|
940
|
-
session
|
|
1140
|
+
unrefH2Session(session)
|
|
941
1141
|
return false
|
|
942
1142
|
}
|
|
943
|
-
setupUpgradeStream(stream,
|
|
1143
|
+
setupUpgradeStream(stream, state)
|
|
944
1144
|
|
|
945
1145
|
return true
|
|
946
1146
|
}
|
|
@@ -968,6 +1168,8 @@ function writeH2 (client, request) {
|
|
|
968
1168
|
method === 'PROPPATCH'
|
|
969
1169
|
)
|
|
970
1170
|
|
|
1171
|
+
let body = state.body
|
|
1172
|
+
|
|
971
1173
|
if (body && typeof body.read === 'function') {
|
|
972
1174
|
// Try to read EOF in order to get length.
|
|
973
1175
|
body.read(0)
|
|
@@ -1014,7 +1216,7 @@ function writeH2 (client, request) {
|
|
|
1014
1216
|
headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}`
|
|
1015
1217
|
}
|
|
1016
1218
|
|
|
1017
|
-
session
|
|
1219
|
+
refH2Session(session)
|
|
1018
1220
|
|
|
1019
1221
|
if (channels.sendHeaders.hasSubscribers) {
|
|
1020
1222
|
let header = ''
|
|
@@ -1026,26 +1228,16 @@ function writeH2 (client, request) {
|
|
|
1026
1228
|
|
|
1027
1229
|
// TODO(metcoder95): add support for sending trailers
|
|
1028
1230
|
const shouldEndStream = body === null || contentLength === 0
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
contentLength,
|
|
1034
|
-
expectsPayload,
|
|
1035
|
-
finalizeRequest,
|
|
1036
|
-
request,
|
|
1037
|
-
headersTimeout,
|
|
1038
|
-
bodyTimeout,
|
|
1039
|
-
responseReceived: false,
|
|
1040
|
-
session,
|
|
1041
|
-
stream: null
|
|
1042
|
-
}
|
|
1231
|
+
|
|
1232
|
+
state.body = body
|
|
1233
|
+
state.contentLength = contentLength
|
|
1234
|
+
state.expectsPayload = expectsPayload
|
|
1043
1235
|
|
|
1044
1236
|
if (expectContinue) {
|
|
1045
1237
|
headers[HTTP2_HEADER_EXPECT] = '100-continue'
|
|
1046
1238
|
}
|
|
1047
1239
|
|
|
1048
|
-
stream =
|
|
1240
|
+
stream = openStream(client, request, session, abort, headers, { endStream: shouldEndStream, signal })
|
|
1049
1241
|
if (stream == null) {
|
|
1050
1242
|
return false
|
|
1051
1243
|
}
|
|
@@ -1056,22 +1248,31 @@ function writeH2 (client, request) {
|
|
|
1056
1248
|
// Increment counter as we have new streams open
|
|
1057
1249
|
clearHttp2IdleTimeout(session)
|
|
1058
1250
|
++session[kOpenStreams]
|
|
1059
|
-
|
|
1251
|
+
|
|
1252
|
+
if (headersTimeout) {
|
|
1253
|
+
stream.setTimeout(headersTimeout)
|
|
1254
|
+
}
|
|
1060
1255
|
|
|
1061
1256
|
stream[kHTTP2Session] = session
|
|
1062
|
-
stream.
|
|
1257
|
+
stream.on('close', completeRequestStream)
|
|
1063
1258
|
|
|
1064
1259
|
bindRequestToStream(request, stream, releaseRequestStream)
|
|
1065
1260
|
if (expectContinue) {
|
|
1066
1261
|
stream.once('continue', writeBodyH2)
|
|
1067
1262
|
}
|
|
1068
|
-
|
|
1069
|
-
stream
|
|
1070
|
-
|
|
1071
|
-
stream.
|
|
1263
|
+
// The handlers below either remove themselves on first invocation or
|
|
1264
|
+
// become unreachable once the stream closes, so plain `on` avoids the
|
|
1265
|
+
// per-listener `once` wrapper allocation.
|
|
1266
|
+
stream.on('response', onResponse)
|
|
1267
|
+
stream.on('headers', onInterimResponse)
|
|
1268
|
+
stream.on('end', onEnd)
|
|
1269
|
+
stream.on('error', onError)
|
|
1270
|
+
stream.on('frameError', onFrameError)
|
|
1072
1271
|
stream.on('aborted', onAborted)
|
|
1073
|
-
|
|
1074
|
-
|
|
1272
|
+
if (headersTimeout || bodyTimeout) {
|
|
1273
|
+
stream.on('timeout', onTimeout)
|
|
1274
|
+
}
|
|
1275
|
+
stream.on('trailers', onTrailers)
|
|
1075
1276
|
|
|
1076
1277
|
if (!expectContinue) {
|
|
1077
1278
|
writeBodyH2.call(stream)
|
|
@@ -1084,6 +1285,7 @@ function removeRequestStreamListeners (stream) {
|
|
|
1084
1285
|
stream.off('error', noop)
|
|
1085
1286
|
stream.off('continue', writeBodyH2)
|
|
1086
1287
|
stream.off('response', onResponse)
|
|
1288
|
+
stream.off('headers', onInterimResponse)
|
|
1087
1289
|
stream.off('end', onEnd)
|
|
1088
1290
|
stream.off('error', onError)
|
|
1089
1291
|
stream.off('frameError', onFrameError)
|
|
@@ -1109,45 +1311,105 @@ function releaseRequestStream (stream) {
|
|
|
1109
1311
|
detachRequestFromStream(request)
|
|
1110
1312
|
}
|
|
1111
1313
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1314
|
+
// A closed or destroyed stream cannot emit further events; leaving the
|
|
1315
|
+
// listeners in place saves the removal scans (they are collected with
|
|
1316
|
+
// the stream). All handlers bail out when the stream state is gone.
|
|
1114
1317
|
if (!stream.destroyed && !stream.closed) {
|
|
1318
|
+
removeRequestStreamListeners(stream)
|
|
1115
1319
|
stream.once('error', noop)
|
|
1116
1320
|
}
|
|
1117
1321
|
}
|
|
1118
1322
|
|
|
1119
1323
|
function onData (chunk) {
|
|
1120
1324
|
const stream = this
|
|
1121
|
-
const
|
|
1325
|
+
const state = stream[kRequestStreamState]
|
|
1326
|
+
|
|
1327
|
+
if (state == null) {
|
|
1328
|
+
return
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
const { request, maxResponseSize } = state
|
|
1122
1332
|
|
|
1123
1333
|
if (request.aborted || request.completed) {
|
|
1124
1334
|
return
|
|
1125
1335
|
}
|
|
1126
1336
|
|
|
1337
|
+
if (maxResponseSize > -1 && state.bytesRead + chunk.length > maxResponseSize) {
|
|
1338
|
+
// Unlike HTTP/1.1, which destroys the socket because it cannot abandon one
|
|
1339
|
+
// response without losing framing, resetting the offending stream leaves
|
|
1340
|
+
// the session usable for its siblings.
|
|
1341
|
+
state.abort(new ResponseExceededMaxSizeError())
|
|
1342
|
+
return
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
state.bytesRead += chunk.length
|
|
1346
|
+
|
|
1127
1347
|
if (request.onResponseData(chunk) === false) {
|
|
1128
1348
|
stream.pause()
|
|
1129
1349
|
}
|
|
1130
1350
|
}
|
|
1131
1351
|
|
|
1352
|
+
function onInterimResponse (headers) {
|
|
1353
|
+
const stream = this
|
|
1354
|
+
const state = stream[kRequestStreamState]
|
|
1355
|
+
|
|
1356
|
+
if (state == null) {
|
|
1357
|
+
return
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const { request } = state
|
|
1361
|
+
|
|
1362
|
+
if (request.aborted || request.completed) {
|
|
1363
|
+
return
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// node http2 emits 'headers' for interim (1xx) informational responses,
|
|
1367
|
+
// while the final response arrives via 'response'. Forward these to the
|
|
1368
|
+
// handler so that onInfo is invoked, matching the HTTP/1 behaviour and the
|
|
1369
|
+
// documented onInfo contract.
|
|
1370
|
+
const statusCode = headers[HTTP2_HEADER_STATUS]
|
|
1371
|
+
delete headers[HTTP2_HEADER_STATUS]
|
|
1372
|
+
|
|
1373
|
+
request.onResponseStart(Number(statusCode), headers, noop, '')
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1132
1376
|
function onResponse (headers) {
|
|
1133
1377
|
const stream = this
|
|
1134
1378
|
const state = stream[kRequestStreamState]
|
|
1379
|
+
|
|
1380
|
+
if (state == null) {
|
|
1381
|
+
return
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1135
1384
|
const { request } = state
|
|
1136
1385
|
|
|
1137
1386
|
stream.off('response', onResponse)
|
|
1138
1387
|
|
|
1388
|
+
// Final response received while still awaiting 100 (Continue): the body won't
|
|
1389
|
+
// be sent, so close our half or the stream stays open and never completes.
|
|
1390
|
+
if (state.body != null && !state.bodySent && !stream.writableEnded) {
|
|
1391
|
+
stream.removeListener('continue', writeBodyH2)
|
|
1392
|
+
stream.end()
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1139
1395
|
const statusCode = headers[HTTP2_HEADER_STATUS]
|
|
1140
1396
|
delete headers[HTTP2_HEADER_STATUS]
|
|
1141
1397
|
request.onResponseStarted()
|
|
1142
1398
|
state.responseReceived = true
|
|
1143
|
-
|
|
1399
|
+
|
|
1400
|
+
if (state.headersTimeout || state.bodyTimeout) {
|
|
1401
|
+
stream.setTimeout(state.bodyTimeout)
|
|
1402
|
+
}
|
|
1144
1403
|
|
|
1145
1404
|
// Due to the stream nature, it is possible we face a race condition
|
|
1146
1405
|
// where the stream has been assigned, but the request has been aborted
|
|
1147
|
-
//
|
|
1148
|
-
//
|
|
1149
|
-
//
|
|
1150
|
-
|
|
1406
|
+
// or already completed and headers hasn't been received yet. A late
|
|
1407
|
+
// 'response' delivered after completion would call request.onResponseStart
|
|
1408
|
+
// post-completion, tripping its `assert(!this.completed)` (an uncatchable
|
|
1409
|
+
// throw on the http2 event tick). Guard `completed` here as onEnd/onTrailers
|
|
1410
|
+
// already do; best effort is to release the stream immediately as there's
|
|
1411
|
+
// no value to keep it open.
|
|
1412
|
+
if (request.aborted || request.completed) {
|
|
1151
1413
|
releaseRequestStream(stream)
|
|
1152
1414
|
return
|
|
1153
1415
|
}
|
|
@@ -1162,17 +1424,25 @@ function onResponse (headers) {
|
|
|
1162
1424
|
function onEnd () {
|
|
1163
1425
|
const stream = this
|
|
1164
1426
|
const state = stream[kRequestStreamState]
|
|
1427
|
+
|
|
1428
|
+
if (state == null) {
|
|
1429
|
+
return
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1165
1432
|
const { request } = state
|
|
1166
1433
|
|
|
1167
1434
|
stream.off('end', onEnd)
|
|
1168
1435
|
|
|
1169
|
-
//
|
|
1170
|
-
//
|
|
1171
|
-
// onTrailers (which may fire after 'end' on Windows) can
|
|
1172
|
-
// store trailers first.
|
|
1436
|
+
// onTrailers (which may fire after 'end' on Windows) has already stored
|
|
1437
|
+
// trailers on the state by now, so completing here still delivers them.
|
|
1173
1438
|
if (state.responseReceived) {
|
|
1174
1439
|
if (!request.aborted && !request.completed) {
|
|
1175
1440
|
state.pendingEnd = true
|
|
1441
|
+
|
|
1442
|
+
// Complete on 'end': a blocked event loop can keep the stream's 'close'
|
|
1443
|
+
// from firing, stranding its buffers until OOM. Idempotent, so a later
|
|
1444
|
+
// 'close' no-ops.
|
|
1445
|
+
completeRequestStream.call(stream)
|
|
1176
1446
|
}
|
|
1177
1447
|
} else {
|
|
1178
1448
|
// Stream ended without receiving a response - this is an error
|
|
@@ -1181,11 +1451,59 @@ function onEnd () {
|
|
|
1181
1451
|
}
|
|
1182
1452
|
}
|
|
1183
1453
|
|
|
1454
|
+
function retryRefusedStream (stream, state) {
|
|
1455
|
+
const { client, request } = state
|
|
1456
|
+
|
|
1457
|
+
if (
|
|
1458
|
+
state.responseReceived ||
|
|
1459
|
+
request.aborted ||
|
|
1460
|
+
request.completed ||
|
|
1461
|
+
request[kRefusedStreamRetry] ||
|
|
1462
|
+
!canReplayRequest(request)
|
|
1463
|
+
) {
|
|
1464
|
+
return false
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// RFC 9113 section 8.7 permits retrying REFUSED_STREAM, but says clients
|
|
1468
|
+
// SHOULD NOT automatically retry the same request more than once.
|
|
1469
|
+
request[kRefusedStreamRetry] = true
|
|
1470
|
+
|
|
1471
|
+
// Detach the failed attempt before moving the request back to the pending
|
|
1472
|
+
// queue. The peer only reset this stream, so the HTTP/2 session remains
|
|
1473
|
+
// usable for the retry. Severing also drops the 'close' listener, so the
|
|
1474
|
+
// abandoned stream cannot later complete the retried request.
|
|
1475
|
+
detachRequestStreamForClose(request)
|
|
1476
|
+
state.stream = null
|
|
1477
|
+
state.requestFinalized = true
|
|
1478
|
+
|
|
1479
|
+
completeRequest(client, request)
|
|
1480
|
+
client[kQueue].splice(client[kPendingIdx], 0, request)
|
|
1481
|
+
client[kResume]()
|
|
1482
|
+
|
|
1483
|
+
return true
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1184
1486
|
function onError (err) {
|
|
1185
1487
|
const stream = this
|
|
1186
1488
|
const state = stream[kRequestStreamState]
|
|
1187
1489
|
|
|
1490
|
+
if (state == null) {
|
|
1491
|
+
return
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1188
1494
|
stream.off('error', onError)
|
|
1495
|
+
|
|
1496
|
+
if (typeof stream.rstCode === 'number' && stream.rstCode !== NGHTTP2_NO_ERROR) {
|
|
1497
|
+
err.http2ErrorCode = stream.rstCode
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
if (
|
|
1501
|
+
stream.rstCode === NGHTTP2_REFUSED_STREAM &&
|
|
1502
|
+
retryRefusedStream(stream, state)
|
|
1503
|
+
) {
|
|
1504
|
+
return
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1189
1507
|
state.abort(err)
|
|
1190
1508
|
}
|
|
1191
1509
|
|
|
@@ -1193,6 +1511,10 @@ function onFrameError (type, code) {
|
|
|
1193
1511
|
const stream = this
|
|
1194
1512
|
const state = stream[kRequestStreamState]
|
|
1195
1513
|
|
|
1514
|
+
if (state == null) {
|
|
1515
|
+
return
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1196
1518
|
stream.off('frameError', onFrameError)
|
|
1197
1519
|
state.abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
|
|
1198
1520
|
}
|
|
@@ -1205,6 +1527,10 @@ function onTimeout () {
|
|
|
1205
1527
|
const stream = this
|
|
1206
1528
|
const state = stream[kRequestStreamState]
|
|
1207
1529
|
|
|
1530
|
+
if (state == null) {
|
|
1531
|
+
return
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1208
1534
|
// Remove self so timeout doesn't fire again after we handle it
|
|
1209
1535
|
stream.off('timeout', onTimeout)
|
|
1210
1536
|
|
|
@@ -1217,6 +1543,11 @@ function onTimeout () {
|
|
|
1217
1543
|
function onTrailers (trailers) {
|
|
1218
1544
|
const stream = this
|
|
1219
1545
|
const state = stream[kRequestStreamState]
|
|
1546
|
+
|
|
1547
|
+
if (state == null) {
|
|
1548
|
+
return
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1220
1551
|
const { request } = state
|
|
1221
1552
|
|
|
1222
1553
|
stream.off('trailers', onTrailers)
|
|
@@ -1226,13 +1557,14 @@ function onTrailers (trailers) {
|
|
|
1226
1557
|
return
|
|
1227
1558
|
}
|
|
1228
1559
|
|
|
1229
|
-
// Store trailers for
|
|
1560
|
+
// Store trailers for completeRequestStream to use when completing
|
|
1230
1561
|
state.trailers = trailers
|
|
1231
1562
|
}
|
|
1232
1563
|
|
|
1233
1564
|
function writeBodyH2 () {
|
|
1234
1565
|
const stream = this
|
|
1235
1566
|
const state = stream[kRequestStreamState]
|
|
1567
|
+
state.bodySent = true
|
|
1236
1568
|
const { abort, body, client, contentLength, expectsPayload, request } = state
|
|
1237
1569
|
|
|
1238
1570
|
if (!body || contentLength === 0) {
|