@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.
Files changed (179) hide show
  1. package/dist/action-runtime-api.js +1 -1
  2. package/dist/{api-CjnGBhM3.js → api-1GXlSrYP.js} +42 -21
  3. package/dist/api.js +14 -14
  4. package/dist/{approval-handler.runtime-BWPQrzpP.js → approval-handler.runtime-Dv4p7REU.js} +3 -3
  5. package/dist/{audit-BwJ_2OjL.js → audit-DEVi9nIs.js} +3 -3
  6. package/dist/{channel-Ddnonoy4.js → channel-CQIRiosn.js} +15 -15
  7. package/dist/{channel-actions-CwmIqnYN.js → channel-actions-JziPEcBU.js} +2 -2
  8. package/dist/{channel-actions.runtime-DvpTfaJ4.js → channel-actions.runtime-D7L47LY3.js} +5 -5
  9. package/dist/channel-plugin-api.js +1 -1
  10. package/dist/{channel.setup-glZ5Y98a.js → channel.setup-oMeR19Cc.js} +2 -2
  11. package/dist/{components-j6lTZBsT.js → components-_s5UPJqU.js} +1 -1
  12. package/dist/{conversation-identity-36FFBwm5.js → conversation-identity-B0k94-bz.js} +2 -2
  13. package/dist/{directory-config-BYMFzL29.js → directory-config-CD1svkyN.js} +1 -1
  14. package/dist/directory-contract-api.js +1 -1
  15. package/dist/{directory-live-9JtJCiu8.js → directory-live-DBTsSRxl.js} +5 -5
  16. package/dist/{handle-action.guild-admin-xxGYrp-I.js → handle-action.guild-admin-CYIFMxh-.js} +2 -2
  17. package/dist/index.js +1 -1
  18. package/dist/{manager.runtime-BK6cf2Zu.js → manager.runtime-BZVdGX2d.js} +17 -9
  19. package/dist/{message-handler-BqpJIavO.js → message-handler-DZd9Bpq8.js} +7 -6
  20. package/dist/{message-handler.preflight-C90MxCcv.js → message-handler.preflight-Axv9J9EO.js} +12 -11
  21. package/dist/{message-handler.process-CeQ7Bd_3.js → message-handler.process-CG0zFS-Z.js} +10 -10
  22. package/dist/{message-utils-CJeAoISH.js → message-utils-Dbuw27gd.js} +1 -1
  23. package/dist/{outbound-adapter-Dxs6iKEq.js → outbound-adapter-CKC_kmUN.js} +6 -6
  24. package/dist/pluralkit-CfTReLl3.js +40 -0
  25. package/dist/{probe-esE_Oqdm.js → probe-DcgZIpyF.js} +3 -2
  26. package/dist/{probe.runtime-C3Atgpvr.js → probe.runtime-CVwM9L5E.js} +1 -1
  27. package/dist/{provider-C7376XFH.js → provider-CwwSXIIj.js} +102 -37
  28. package/dist/{provider-session.runtime-CCdqogGf.js → provider-session.runtime-reBWkqHZ.js} +3 -3
  29. package/dist/provider.runtime-BsmTLYS_.js +2 -0
  30. package/dist/{resolve-allowlist-common-3gB2Gpmm.js → resolve-allowlist-common-D7bZqAw3.js} +3 -3
  31. package/dist/{resolve-channels-B3gQf8QL.js → resolve-channels--oE-RnO6.js} +6 -6
  32. package/dist/{resolve-users-BO10y5fJ.js → resolve-users-QXo8CXBU.js} +5 -5
  33. package/dist/{runtime-CFlR3L9X.js → runtime-CF7ramAs.js} +7 -7
  34. package/dist/runtime-api.actions.js +2 -2
  35. package/dist/runtime-api.js +19 -19
  36. package/dist/runtime-api.lookup.js +5 -5
  37. package/dist/runtime-api.monitor-CzF8MUl_.js +6 -0
  38. package/dist/runtime-api.monitor.js +5 -5
  39. package/dist/runtime-api.send.js +5 -5
  40. package/dist/runtime-api.threads.js +3 -3
  41. package/dist/{send-JHxfT4Pk.js → send-Bb295iHT.js} +5 -5
  42. package/dist/{send.components-CN35E2Ck.js → send.components-COkDUO3B.js} +4 -4
  43. package/dist/{send.outbound-B-cA1Esv.js → send.outbound-B_sk0hNe.js} +3 -3
  44. package/dist/{send.receipt-CGecJDin.js → send.receipt-Cli6_9Sn.js} +19 -7
  45. package/dist/{send.shared-vcbfThdy.js → send.shared-BCG7MWnP.js} +2 -2
  46. package/dist/session-binding-contract-api.js +1 -1
  47. package/dist/setup-plugin-api.js +1 -1
  48. package/dist/{subagent-hooks-20wXhVzZ.js → subagent-hooks-CtqoWK4x.js} +2 -2
  49. package/dist/subagent-hooks-api.js +1 -1
  50. package/dist/{system-events-DR27TE7p.js → system-events-BxGNk_wE.js} +1 -1
  51. package/dist/{target-resolver-CDi1gI90.js → target-resolver-BcoK6b0g.js} +2 -2
  52. package/dist/targets-Q-ZoP7K4.js +3 -0
  53. package/dist/{thread-bindings-DU4T6nDo.js → thread-bindings-DJ_HT6GJ.js} +4 -4
  54. package/dist/{thread-bindings.discord-api-CYkr-3Gj.js → thread-bindings.discord-api-OPyjvgyz.js} +4 -4
  55. package/dist/{thread-bindings.manager-BqWs1UE2.js → thread-bindings.manager-Cd2HAVw_.js} +3 -3
  56. package/dist/{transcripts-source-D-lBI5ZD.js → transcripts-source-CPf5gURy.js} +1 -1
  57. package/dist/transcripts-source-api.js +1 -1
  58. package/dist/{typing-xr05bL-r.js → typing-DY0cwLdg.js} +1 -1
  59. package/node_modules/undici/README.md +8 -2
  60. package/node_modules/undici/docs/docs/api/Agent.md +248 -44
  61. package/node_modules/undici/docs/docs/api/BalancedPool.md +246 -48
  62. package/node_modules/undici/docs/docs/api/CacheStorage.md +171 -13
  63. package/node_modules/undici/docs/docs/api/CacheStore.md +294 -98
  64. package/node_modules/undici/docs/docs/api/Client.md +386 -179
  65. package/node_modules/undici/docs/docs/api/ClientStats.md +80 -14
  66. package/node_modules/undici/docs/docs/api/Connector.md +118 -36
  67. package/node_modules/undici/docs/docs/api/ContentType.md +65 -25
  68. package/node_modules/undici/docs/docs/api/Cookies.md +122 -71
  69. package/node_modules/undici/docs/docs/api/Debug.md +34 -18
  70. package/node_modules/undici/docs/docs/api/DiagnosticsChannel.md +364 -164
  71. package/node_modules/undici/docs/docs/api/Dispatcher.md +485 -1102
  72. package/node_modules/undici/docs/docs/api/EnvHttpProxyAgent.md +102 -87
  73. package/node_modules/undici/docs/docs/api/Errors.md +503 -40
  74. package/node_modules/undici/docs/docs/api/EventSource.md +298 -32
  75. package/node_modules/undici/docs/docs/api/Fetch.md +680 -31
  76. package/node_modules/undici/docs/docs/api/GlobalInstallation.md +78 -98
  77. package/node_modules/undici/docs/docs/api/H2CClient.md +298 -176
  78. package/node_modules/undici/docs/docs/api/Interceptors.md +418 -0
  79. package/node_modules/undici/docs/docs/api/MockAgent.md +333 -314
  80. package/node_modules/undici/docs/docs/api/MockCallHistory.md +272 -98
  81. package/node_modules/undici/docs/docs/api/MockCallHistoryLog.md +189 -26
  82. package/node_modules/undici/docs/docs/api/MockClient.md +151 -33
  83. package/node_modules/undici/docs/docs/api/MockErrors.md +56 -5
  84. package/node_modules/undici/docs/docs/api/MockPool.md +321 -258
  85. package/node_modules/undici/docs/docs/api/Pool.md +235 -43
  86. package/node_modules/undici/docs/docs/api/PoolStats.md +119 -13
  87. package/node_modules/undici/docs/docs/api/ProxyAgent.md +179 -160
  88. package/node_modules/undici/docs/docs/api/RedirectHandler.md +238 -63
  89. package/node_modules/undici/docs/docs/api/RetryAgent.md +113 -26
  90. package/node_modules/undici/docs/docs/api/RetryHandler.md +161 -77
  91. package/node_modules/undici/docs/docs/api/RoundRobinPool.md +166 -72
  92. package/node_modules/undici/docs/docs/api/SnapshotAgent.md +264 -542
  93. package/node_modules/undici/docs/docs/api/Socks5ProxyAgent.md +177 -196
  94. package/node_modules/undici/docs/docs/api/Util.md +68 -11
  95. package/node_modules/undici/docs/docs/api/WebSocket.md +475 -80
  96. package/node_modules/undici/docs/docs/api/api-lifecycle.md +100 -32
  97. package/node_modules/undici/docs/docs/best-practices/client-certificate.md +2 -0
  98. package/node_modules/undici/docs/docs/best-practices/crawling.md +3 -1
  99. package/node_modules/undici/docs/docs/best-practices/migrating-from-v7-to-v8.md +6 -4
  100. package/node_modules/undici/docs/docs/best-practices/mocking-request.md +9 -7
  101. package/node_modules/undici/docs/docs/best-practices/proxy.md +3 -1
  102. package/node_modules/undici/docs/docs/best-practices/undici-vs-builtin-fetch.md +10 -8
  103. package/node_modules/undici/docs/docs/best-practices/writing-tests.md +2 -0
  104. package/node_modules/undici/docs/docs/{GettingStarted.md → getting-started.md} +22 -19
  105. package/node_modules/undici/docs/docs/index.md +784 -0
  106. package/node_modules/undici/docs/docs/site.json +125 -0
  107. package/node_modules/undici/docs/docs/type-map.json +79 -0
  108. package/node_modules/undici/lib/api/api-request.js +7 -1
  109. package/node_modules/undici/lib/api/readable.js +42 -7
  110. package/node_modules/undici/lib/cache/memory-cache-store.js +56 -11
  111. package/node_modules/undici/lib/cache/sqlite-cache-store.js +7 -1
  112. package/node_modules/undici/lib/core/connect.js +16 -2
  113. package/node_modules/undici/lib/core/errors.js +20 -0
  114. package/node_modules/undici/lib/core/request.js +14 -3
  115. package/node_modules/undici/lib/core/symbols.js +3 -0
  116. package/node_modules/undici/lib/core/util.js +30 -2
  117. package/node_modules/undici/lib/dispatcher/agent.js +10 -5
  118. package/node_modules/undici/lib/dispatcher/balanced-pool.js +7 -4
  119. package/node_modules/undici/lib/dispatcher/client-h1.js +74 -9
  120. package/node_modules/undici/lib/dispatcher/client-h2.js +483 -151
  121. package/node_modules/undici/lib/dispatcher/client.js +76 -30
  122. package/node_modules/undici/lib/dispatcher/dispatcher-base.js +13 -1
  123. package/node_modules/undici/lib/dispatcher/dispatcher.js +15 -1
  124. package/node_modules/undici/lib/dispatcher/dispatcher1-wrapper.js +3 -0
  125. package/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js +37 -8
  126. package/node_modules/undici/lib/dispatcher/proxy-agent.js +58 -7
  127. package/node_modules/undici/lib/dispatcher/retry-agent.js +3 -0
  128. package/node_modules/undici/lib/dispatcher/round-robin-pool.js +1 -1
  129. package/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js +106 -22
  130. package/node_modules/undici/lib/global.js +36 -16
  131. package/node_modules/undici/lib/handler/cache-handler.js +297 -65
  132. package/node_modules/undici/lib/handler/cache-revalidation-handler.js +13 -3
  133. package/node_modules/undici/lib/handler/decorator-handler.js +7 -1
  134. package/node_modules/undici/lib/handler/deduplication-handler.js +12 -6
  135. package/node_modules/undici/lib/handler/redirect-handler.js +26 -5
  136. package/node_modules/undici/lib/handler/retry-handler.js +238 -51
  137. package/node_modules/undici/lib/interceptor/cache.js +221 -58
  138. package/node_modules/undici/lib/interceptor/decompress.js +124 -15
  139. package/node_modules/undici/lib/interceptor/deduplicate.js +5 -4
  140. package/node_modules/undici/lib/interceptor/dns.js +7 -0
  141. package/node_modules/undici/lib/interceptor/dump.js +10 -23
  142. package/node_modules/undici/lib/mock/mock-agent.js +13 -1
  143. package/node_modules/undici/lib/mock/mock-interceptor.js +25 -7
  144. package/node_modules/undici/lib/mock/mock-utils.js +246 -27
  145. package/node_modules/undici/lib/util/cache.js +415 -71
  146. package/node_modules/undici/lib/util/date.js +23 -6
  147. package/node_modules/undici/lib/web/cookies/parse.js +150 -146
  148. package/node_modules/undici/lib/web/cookies/util.js +79 -9
  149. package/node_modules/undici/lib/web/eventsource/eventsource-stream.js +28 -1
  150. package/node_modules/undici/lib/web/eventsource/eventsource.js +13 -3
  151. package/node_modules/undici/lib/web/eventsource/util.js +1 -1
  152. package/node_modules/undici/lib/web/fetch/body.js +2 -1
  153. package/node_modules/undici/lib/web/fetch/constants.js +1 -1
  154. package/node_modules/undici/lib/web/fetch/index.js +17 -2
  155. package/node_modules/undici/lib/web/fetch/request.js +2 -3
  156. package/node_modules/undici/lib/web/fetch/response.js +1 -3
  157. package/node_modules/undici/lib/web/fetch/util.js +10 -2
  158. package/node_modules/undici/lib/web/webidl/index.js +3 -3
  159. package/node_modules/undici/lib/web/websocket/connection.js +1 -1
  160. package/node_modules/undici/lib/web/websocket/permessage-deflate.js +5 -0
  161. package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +8 -10
  162. package/node_modules/undici/lib/web/websocket/websocket.js +22 -0
  163. package/node_modules/undici/package.json +4 -4
  164. package/node_modules/undici/types/client.d.ts +53 -3
  165. package/node_modules/undici/types/cookies.d.ts +1 -1
  166. package/node_modules/undici/types/dispatcher.d.ts +3 -3
  167. package/node_modules/undici/types/errors.d.ts +10 -0
  168. package/node_modules/undici/types/handlers.d.ts +2 -0
  169. package/node_modules/undici/types/interceptors.d.ts +2 -0
  170. package/node_modules/undici/types/mock-agent.d.ts +11 -10
  171. package/node_modules/undici/types/mock-interceptor.d.ts +5 -1
  172. package/node_modules/undici/types/proxy-agent.d.ts +7 -0
  173. package/node_modules/undici/types/socks5-proxy-agent.d.ts +2 -0
  174. package/npm-shrinkwrap.json +7 -7
  175. package/package.json +6 -5
  176. package/dist/pluralkit-DI6RYaVj.js +0 -24
  177. package/dist/provider.runtime-D1m66kDS.js +0 -2
  178. package/dist/runtime-api.monitor-4oc58TSk.js +0 -6
  179. 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 index = client[kQueue].indexOf(request, client[kRunningIdx])
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
- client[kQueue].splice(index, 1)
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 canRetryRequestAfterGoAway (request) {
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
- function closeRequestStream (request, code = NGHTTP2_REFUSED_STREAM) {
178
- const stream = request[kRequestStream]
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
- clearRequestStream(request)
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[kHTTP2InitialWindowSize]
193
- const http2ConnectionWindowSize = client[kHTTP2ConnectionWindowSize]
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[kMaxConcurrentStreams],
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[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
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.unref()
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
- socket.unref()
333
- session.unref()
410
+ unrefH2Session(session)
334
411
  } else {
335
- socket.ref()
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
- closeRequestStream(request)
643
+ streamsToClose.push(detachRequestStreamForClose(request))
494
644
 
495
- if (canRetryRequestAfterGoAway(request)) {
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[kClient][kOnError](err)
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.unref()
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
- function onRequestStreamClose () {
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
- // Release the stream first so request references are cleared,
637
- // then complete the response with trailers if available.
638
- releaseRequestStream(this)
797
+ if (state == null) {
798
+ return
799
+ }
639
800
 
640
- if (state.pendingEnd && !state.request.aborted && !state.request.completed) {
641
- state.request.onResponseEnd(state.trailers || {})
642
- state.finalizeRequest()
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
- this.off('data', onData)
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
- state.finalizeRequest()
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
- let requestFinalized = false
814
- const finalizeRequest = (resetPendingIdx = false) => {
815
- if (requestFinalized) {
816
- return
817
- }
818
-
819
- requestFinalized = true
820
- completeRequest(client, request, resetPendingIdx)
821
-
822
- client[kResume]()
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
- const requestStream = (headers, options) => {
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
- return null
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.ref()
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.unref()
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 = requestStream(headers, { endStream: false, signal })
1124
+ stream = openStream(client, request, session, abort, headers, { endStream: false, signal })
925
1125
  if (stream == null) {
926
- session.unref()
1126
+ unrefH2Session(session)
927
1127
  return false
928
1128
  }
929
- setupUpgradeStream(stream, upgradeState)
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 = requestStream(headers, { endStream: false, signal })
1138
+ stream = openStream(client, request, session, abort, headers, { endStream: false, signal })
939
1139
  if (stream == null) {
940
- session.unref()
1140
+ unrefH2Session(session)
941
1141
  return false
942
1142
  }
943
- setupUpgradeStream(stream, upgradeState)
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.ref()
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
- const state = {
1030
- abort,
1031
- body,
1032
- client,
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 = requestStream(headers, { endStream: shouldEndStream, signal })
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
- stream.setTimeout(headersTimeout)
1251
+
1252
+ if (headersTimeout) {
1253
+ stream.setTimeout(headersTimeout)
1254
+ }
1060
1255
 
1061
1256
  stream[kHTTP2Session] = session
1062
- stream.once('close', onRequestStreamClose)
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
- stream.once('response', onResponse)
1069
- stream.once('end', onEnd)
1070
- stream.once('error', onError)
1071
- stream.once('frameError', onFrameError)
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
- stream.on('timeout', onTimeout)
1074
- stream.once('trailers', onTrailers)
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
- removeRequestStreamListeners(stream)
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 { request } = stream[kRequestStreamState]
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
- stream.setTimeout(state.bodyTimeout)
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
- // the request remains in-flight and headers hasn't been received yet
1148
- // for those scenarios, best effort is to destroy the stream immediately
1149
- // as there's no value to keep it open.
1150
- if (request.aborted) {
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
- // If we received a response, this is a normal completion.
1170
- // Defer actual completion to onRequestStreamClose so that
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 onRequestStreamClose to use when completing
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) {