@openclaw/discord 2026.6.33 → 2026.6.34

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 (103) hide show
  1. package/dist/api.js +1 -1
  2. package/dist/{channel-Ddnonoy4.js → channel-DU4jnQgl.js} +1 -1
  3. package/dist/channel-plugin-api.js +1 -1
  4. package/dist/{manager.runtime-BK6cf2Zu.js → manager.runtime-NxCd-PVt.js} +1 -1
  5. package/dist/{message-handler-BqpJIavO.js → message-handler-cizxbaDc.js} +3 -2
  6. package/dist/{message-handler.preflight-C90MxCcv.js → message-handler.preflight-CbD0u7U7.js} +2 -2
  7. package/dist/{message-handler.process-CeQ7Bd_3.js → message-handler.process-Do4EM591.js} +2 -2
  8. package/dist/{provider-C7376XFH.js → provider-B-s93q9s.js} +31 -6
  9. package/dist/{provider-session.runtime-CCdqogGf.js → provider-session.runtime-BKMOqHpd.js} +1 -1
  10. package/dist/provider.runtime-CaTJrpnl.js +2 -0
  11. package/dist/runtime-api.js +3 -3
  12. package/dist/{runtime-api.monitor-4oc58TSk.js → runtime-api.monitor-CRiBIQEI.js} +2 -2
  13. package/dist/runtime-api.monitor.js +3 -3
  14. package/node_modules/undici/README.md +8 -2
  15. package/node_modules/undici/docs/docs/api/Agent.md +248 -44
  16. package/node_modules/undici/docs/docs/api/BalancedPool.md +246 -48
  17. package/node_modules/undici/docs/docs/api/CacheStorage.md +171 -13
  18. package/node_modules/undici/docs/docs/api/CacheStore.md +294 -98
  19. package/node_modules/undici/docs/docs/api/Client.md +369 -179
  20. package/node_modules/undici/docs/docs/api/ClientStats.md +80 -14
  21. package/node_modules/undici/docs/docs/api/Connector.md +118 -36
  22. package/node_modules/undici/docs/docs/api/ContentType.md +65 -25
  23. package/node_modules/undici/docs/docs/api/Cookies.md +122 -71
  24. package/node_modules/undici/docs/docs/api/Debug.md +34 -18
  25. package/node_modules/undici/docs/docs/api/DiagnosticsChannel.md +364 -164
  26. package/node_modules/undici/docs/docs/api/Dispatcher.md +485 -1102
  27. package/node_modules/undici/docs/docs/api/EnvHttpProxyAgent.md +101 -87
  28. package/node_modules/undici/docs/docs/api/Errors.md +503 -40
  29. package/node_modules/undici/docs/docs/api/EventSource.md +294 -32
  30. package/node_modules/undici/docs/docs/api/Fetch.md +680 -31
  31. package/node_modules/undici/docs/docs/api/GlobalInstallation.md +78 -98
  32. package/node_modules/undici/docs/docs/api/H2CClient.md +298 -176
  33. package/node_modules/undici/docs/docs/api/Interceptors.md +414 -0
  34. package/node_modules/undici/docs/docs/api/MockAgent.md +333 -314
  35. package/node_modules/undici/docs/docs/api/MockCallHistory.md +272 -98
  36. package/node_modules/undici/docs/docs/api/MockCallHistoryLog.md +189 -26
  37. package/node_modules/undici/docs/docs/api/MockClient.md +151 -33
  38. package/node_modules/undici/docs/docs/api/MockErrors.md +56 -5
  39. package/node_modules/undici/docs/docs/api/MockPool.md +321 -258
  40. package/node_modules/undici/docs/docs/api/Pool.md +235 -43
  41. package/node_modules/undici/docs/docs/api/PoolStats.md +119 -13
  42. package/node_modules/undici/docs/docs/api/ProxyAgent.md +179 -160
  43. package/node_modules/undici/docs/docs/api/RedirectHandler.md +238 -63
  44. package/node_modules/undici/docs/docs/api/RetryAgent.md +113 -26
  45. package/node_modules/undici/docs/docs/api/RetryHandler.md +161 -77
  46. package/node_modules/undici/docs/docs/api/RoundRobinPool.md +166 -72
  47. package/node_modules/undici/docs/docs/api/SnapshotAgent.md +264 -542
  48. package/node_modules/undici/docs/docs/api/Socks5ProxyAgent.md +162 -187
  49. package/node_modules/undici/docs/docs/api/Util.md +68 -11
  50. package/node_modules/undici/docs/docs/api/WebSocket.md +475 -80
  51. package/node_modules/undici/docs/docs/api/api-lifecycle.md +100 -32
  52. package/node_modules/undici/docs/docs/best-practices/client-certificate.md +2 -0
  53. package/node_modules/undici/docs/docs/best-practices/crawling.md +3 -1
  54. package/node_modules/undici/docs/docs/best-practices/migrating-from-v7-to-v8.md +6 -4
  55. package/node_modules/undici/docs/docs/best-practices/mocking-request.md +9 -7
  56. package/node_modules/undici/docs/docs/best-practices/proxy.md +3 -1
  57. package/node_modules/undici/docs/docs/best-practices/undici-vs-builtin-fetch.md +10 -8
  58. package/node_modules/undici/docs/docs/best-practices/writing-tests.md +2 -0
  59. package/node_modules/undici/docs/docs/{GettingStarted.md → getting-started.md} +22 -19
  60. package/node_modules/undici/docs/docs/index.md +784 -0
  61. package/node_modules/undici/docs/docs/site.json +125 -0
  62. package/node_modules/undici/docs/docs/type-map.json +79 -0
  63. package/node_modules/undici/lib/api/api-request.js +7 -1
  64. package/node_modules/undici/lib/api/readable.js +47 -2
  65. package/node_modules/undici/lib/cache/memory-cache-store.js +54 -9
  66. package/node_modules/undici/lib/cache/sqlite-cache-store.js +7 -1
  67. package/node_modules/undici/lib/core/errors.js +20 -0
  68. package/node_modules/undici/lib/core/request.js +14 -3
  69. package/node_modules/undici/lib/core/util.js +30 -2
  70. package/node_modules/undici/lib/dispatcher/agent.js +3 -3
  71. package/node_modules/undici/lib/dispatcher/client-h1.js +65 -8
  72. package/node_modules/undici/lib/dispatcher/client-h2.js +265 -142
  73. package/node_modules/undici/lib/dispatcher/proxy-agent.js +40 -6
  74. package/node_modules/undici/lib/global.js +36 -16
  75. package/node_modules/undici/lib/handler/cache-handler.js +269 -61
  76. package/node_modules/undici/lib/handler/cache-revalidation-handler.js +13 -3
  77. package/node_modules/undici/lib/handler/redirect-handler.js +6 -2
  78. package/node_modules/undici/lib/handler/retry-handler.js +87 -23
  79. package/node_modules/undici/lib/interceptor/cache.js +159 -52
  80. package/node_modules/undici/lib/interceptor/decompress.js +9 -2
  81. package/node_modules/undici/lib/mock/mock-interceptor.js +25 -7
  82. package/node_modules/undici/lib/mock/mock-utils.js +39 -10
  83. package/node_modules/undici/lib/util/cache.js +372 -68
  84. package/node_modules/undici/lib/util/date.js +23 -6
  85. package/node_modules/undici/lib/web/cookies/parse.js +3 -2
  86. package/node_modules/undici/lib/web/cookies/util.js +79 -9
  87. package/node_modules/undici/lib/web/eventsource/util.js +1 -1
  88. package/node_modules/undici/lib/web/fetch/body.js +2 -1
  89. package/node_modules/undici/lib/web/fetch/constants.js +1 -1
  90. package/node_modules/undici/lib/web/fetch/index.js +11 -2
  91. package/node_modules/undici/lib/web/fetch/response.js +1 -3
  92. package/node_modules/undici/lib/web/fetch/util.js +4 -1
  93. package/node_modules/undici/package.json +2 -2
  94. package/node_modules/undici/types/client.d.ts +3 -3
  95. package/node_modules/undici/types/cookies.d.ts +1 -1
  96. package/node_modules/undici/types/dispatcher.d.ts +3 -3
  97. package/node_modules/undici/types/errors.d.ts +10 -0
  98. package/node_modules/undici/types/handlers.d.ts +2 -0
  99. package/node_modules/undici/types/mock-interceptor.d.ts +5 -1
  100. package/node_modules/undici/types/proxy-agent.d.ts +7 -0
  101. package/npm-shrinkwrap.json +7 -7
  102. package/package.json +5 -5
  103. package/dist/provider.runtime-D1m66kDS.js +0 -2
@@ -154,13 +154,24 @@ function requeueUnsentRequest (client, request) {
154
154
  }
155
155
 
156
156
  function completeRequest (client, request, resetPendingIdx = false) {
157
- const index = client[kQueue].indexOf(request, client[kRunningIdx])
157
+ const queue = client[kQueue]
158
+ const runningIdx = client[kRunningIdx]
159
+
160
+ // In-order completion: clear the request and advance without splicing.
161
+ // The client's resume loop compacts cleared slots once the index grows.
162
+ if (runningIdx < client[kPendingIdx] && queue[runningIdx] === request) {
163
+ queue[runningIdx] = null
164
+ client[kRunningIdx] = runningIdx + 1
165
+ return
166
+ }
167
+
168
+ const index = queue.indexOf(request, runningIdx)
158
169
 
159
170
  if (index === -1 || index >= client[kPendingIdx]) {
160
171
  return
161
172
  }
162
173
 
163
- client[kQueue].splice(index, 1)
174
+ queue.splice(index, 1)
164
175
  client[kPendingIdx]--
165
176
 
166
177
  if (resetPendingIdx && client[kPendingIdx] < client[kRunningIdx]) {
@@ -174,11 +185,7 @@ function canRetryRequestAfterGoAway (request) {
174
185
  return body == null || util.isBuffer(body) || util.isBlobLike(body)
175
186
  }
176
187
 
177
- function closeRequestStream (request, code = NGHTTP2_REFUSED_STREAM) {
178
- const stream = request[kRequestStream]
179
-
180
- clearRequestStream(request)
181
-
188
+ function closeStream (stream, code = NGHTTP2_REFUSED_STREAM) {
182
189
  if (stream != null && !stream.destroyed && !stream.closed) {
183
190
  try {
184
191
  stream.close(code)
@@ -186,6 +193,14 @@ function closeRequestStream (request, code = NGHTTP2_REFUSED_STREAM) {
186
193
  }
187
194
  }
188
195
 
196
+ function detachRequestStreamForClose (request) {
197
+ const stream = request[kRequestStream]
198
+
199
+ clearRequestStream(request)
200
+
201
+ return stream
202
+ }
203
+
189
204
  function connectH2 (client, socket) {
190
205
  client[kSocket] = socket
191
206
 
@@ -208,6 +223,11 @@ function connectH2 (client, socket) {
208
223
  session[kSocket] = socket
209
224
  session[kHTTP2SessionState] = {
210
225
  idleTimeout: null,
226
+ // Sockets start out ref'd. Session ref/unref proxies to the socket, so a
227
+ // single cached flag lets us skip redundant uv ref/unref calls, provided
228
+ // every ref/unref of the session or its socket goes through
229
+ // refH2Session/unrefH2Session.
230
+ refed: true,
211
231
  ping: {
212
232
  interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
213
233
  }
@@ -227,14 +247,13 @@ function connectH2 (client, socket) {
227
247
 
228
248
  util.addListener(session, 'error', onHttp2SessionError)
229
249
  util.addListener(session, 'frameError', onHttp2FrameError)
230
- util.addListener(session, 'end', onHttp2SessionEnd)
231
250
  util.addListener(session, 'goaway', onHttp2SessionGoAway)
232
251
  util.addListener(session, 'close', onHttp2SessionClose)
233
252
  util.addListener(session, 'remoteSettings', onHttp2RemoteSettings)
234
253
  // TODO (@metcoder95): implement SETTINGS support
235
254
  // util.addListener(session, 'localSettings', onHttp2RemoteSettings)
236
255
 
237
- session.unref()
256
+ unrefH2Session(session)
238
257
 
239
258
  client[kHTTP2Session] = session
240
259
  socket[kHTTP2Session] = session
@@ -303,16 +322,6 @@ function connectH2 (client, socket) {
303
322
  // Don't dispatch an upgrade until all preceding requests have completed.
304
323
  // Possibly, we do not have remote settings confirmed yet.
305
324
  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
325
  } else {
317
326
  return (request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false
318
327
  }
@@ -323,17 +332,36 @@ function connectH2 (client, socket) {
323
332
  }
324
333
  }
325
334
 
335
+ // Session ref/unref proxies to the underlying socket, so refH2Session and
336
+ // unrefH2Session cover both and can skip the call when the cached ref state
337
+ // already matches.
338
+ function refH2Session (session) {
339
+ const state = session[kHTTP2SessionState]
340
+
341
+ if (state.refed === false) {
342
+ state.refed = true
343
+ session.ref()
344
+ }
345
+ }
346
+
347
+ function unrefH2Session (session) {
348
+ const state = session[kHTTP2SessionState]
349
+
350
+ if (state.refed === true) {
351
+ state.refed = false
352
+ session.unref()
353
+ }
354
+ }
355
+
326
356
  function resumeH2 (client) {
327
357
  const socket = client[kSocket]
328
358
  const session = client[kHTTP2Session]
329
359
 
330
360
  if (socket?.destroyed === false) {
331
361
  if (client[kSize] === 0 || client[kMaxConcurrentStreams] === 0) {
332
- socket.unref()
333
- session.unref()
362
+ unrefH2Session(session)
334
363
  } else {
335
- socket.ref()
336
- session.ref()
364
+ refH2Session(session)
337
365
  }
338
366
 
339
367
  if (client[kSize] === 0 && session[kOpenStreams] === 0) {
@@ -447,23 +475,26 @@ function onHttp2SessionError (err) {
447
475
  assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
448
476
 
449
477
  this[kSocket][kError] = err
478
+
479
+ if (this[kReceivedGoAway]) {
480
+ return
481
+ }
482
+
450
483
  this[kClient][kOnError](err)
451
484
  }
452
485
 
453
486
  function onHttp2FrameError (type, code, id) {
454
487
  if (id === 0) {
488
+ if (this[kReceivedGoAway]) {
489
+ return
490
+ }
491
+
455
492
  const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
456
493
  this[kSocket][kError] = err
457
494
  this[kClient][kOnError](err)
458
495
  }
459
496
  }
460
497
 
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
498
  /**
468
499
  * This is the root cause of #3011
469
500
  * We need to handle GOAWAY frames properly, and trigger the session close
@@ -485,12 +516,16 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
485
516
  const previousPendingIdx = client[kPendingIdx]
486
517
  const pendingIdx = getGoAwayPendingIdx(client, lastStreamID)
487
518
  const retriableRequests = []
519
+ const streamsToClose = []
488
520
 
521
+ // Closing one stream after GOAWAY can synchronously emit frameError on
522
+ // sibling streams. Detach all affected requests first so those errors do
523
+ // not fail requests that are about to be requeued.
489
524
  for (let i = pendingIdx; i < previousPendingIdx; i++) {
490
525
  const request = client[kQueue][i]
491
526
 
492
527
  if (request != null) {
493
- closeRequestStream(request)
528
+ streamsToClose.push(detachRequestStreamForClose(request))
494
529
 
495
530
  if (canRetryRequestAfterGoAway(request)) {
496
531
  retriableRequests.push(request)
@@ -500,6 +535,10 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
500
535
  }
501
536
  }
502
537
 
538
+ for (let i = 0; i < streamsToClose.length; i++) {
539
+ closeStream(streamsToClose[i])
540
+ }
541
+
503
542
  if (pendingIdx !== previousPendingIdx) {
504
543
  const remainingPendingRequests = client[kQueue].slice(previousPendingIdx)
505
544
  client[kQueue].length = pendingIdx
@@ -595,7 +634,11 @@ function onHttp2SocketError (err) {
595
634
 
596
635
  this[kError] = err
597
636
 
598
- this[kClient][kOnError](err)
637
+ if (this[kHTTP2Session]?.[kReceivedGoAway]) {
638
+ return
639
+ }
640
+
641
+ this[kHTTP2Session]?.[kClient]?.[kOnError](err)
599
642
  }
600
643
 
601
644
  function onHttp2SocketEnd () {
@@ -614,7 +657,7 @@ function closeStreamSession (stream) {
614
657
  stream[kHTTP2Session] = null
615
658
  session[kOpenStreams] -= 1
616
659
  if (session[kOpenStreams] === 0) {
617
- session.unref()
660
+ unrefH2Session(session)
618
661
  setHttp2IdleTimeout(session)
619
662
  }
620
663
  }
@@ -629,22 +672,24 @@ function onUpgradeStreamClose () {
629
672
  closeStreamSession(this)
630
673
  }
631
674
 
632
- function onRequestStreamClose () {
675
+ // Idempotent terminal cleanup, called from both 'end' and 'close': the
676
+ // null-state guard no-ops the later call.
677
+ function completeRequestStream () {
633
678
  const state = this[kRequestStreamState]
634
679
 
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)
680
+ if (state == null) {
681
+ return
682
+ }
683
+
684
+ // Release the stream first so request references are cleared,
685
+ // then complete the response with trailers if available.
686
+ releaseRequestStream(this)
639
687
 
640
- if (state.pendingEnd && !state.request.aborted && !state.request.completed) {
641
- state.request.onResponseEnd(state.trailers || {})
642
- state.finalizeRequest()
643
- }
688
+ if (state.pendingEnd && !state.request.aborted && !state.request.completed) {
689
+ state.request.onResponseEnd(state.trailers || {})
644
690
  }
645
691
 
646
- this.off('data', onData)
647
- this.off('error', noop)
692
+ finalizeRequest(state)
648
693
  closeStreamSession(this)
649
694
  this[kRequestStreamState] = null
650
695
  }
@@ -767,7 +812,7 @@ function onUpgradeResponse (headers, _flags) {
767
812
 
768
813
  removeUpgradeStreamListeners(stream)
769
814
  detachRequestFromStream(request)
770
- state.finalizeRequest()
815
+ finalizeRequest(state)
771
816
  }
772
817
 
773
818
  function setupUpgradeStream (stream, state) {
@@ -790,12 +835,51 @@ function setupUpgradeStream (stream, state) {
790
835
  stream.setTimeout(headersTimeout)
791
836
  }
792
837
 
838
+ function finalizeRequest (state, resetPendingIdx = false) {
839
+ if (state.requestFinalized) {
840
+ return
841
+ }
842
+
843
+ state.requestFinalized = true
844
+ completeRequest(state.client, state.request, resetPendingIdx)
845
+
846
+ state.client[kResume]()
847
+ }
848
+
849
+ function openStream (client, request, session, abort, headers, options) {
850
+ try {
851
+ return session.request(headers, options)
852
+ } catch (err) {
853
+ // A GOAWAY'd session rejects new streams, same as an invalid session:
854
+ // reset and requeue on a fresh connection rather than the destroy + abort
855
+ // below, whose destroy(socket, err) can crash via an unhandled 'error'.
856
+ if (err?.code === 'ERR_HTTP2_INVALID_SESSION' || err?.code === 'ERR_HTTP2_GOAWAY_SESSION') {
857
+ const wrappedErr = new SocketError(err.message, util.getSocketInfo(session[kSocket]))
858
+ wrappedErr.cause = err
859
+ session[kError] = wrappedErr
860
+ resetHttp2Session(session, wrappedErr)
861
+ requeueUnsentRequest(client, request)
862
+
863
+ return null
864
+ }
865
+
866
+ const wrappedErr = new InformationalError(err.message, { cause: err })
867
+ session[kError] = wrappedErr
868
+ session[kSocket][kError] = wrappedErr
869
+
870
+ session.destroy(wrappedErr)
871
+ util.destroy(session[kSocket], wrappedErr)
872
+ abort(wrappedErr)
873
+
874
+ return null
875
+ }
876
+ }
877
+
793
878
  function writeH2 (client, request) {
794
879
  const headersTimeout = request.headersTimeout ?? client[kHeadersTimeout]
795
880
  const bodyTimeout = request.bodyTimeout ?? client[kBodyTimeout]
796
881
  const session = client[kHTTP2Session]
797
882
  const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
798
- let { body } = request
799
883
 
800
884
  if (upgrade != null && upgrade !== 'websocket') {
801
885
  util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`))
@@ -804,22 +888,28 @@ function writeH2 (client, request) {
804
888
 
805
889
  const headers = buildRequestHeaders(reqHeaders)
806
890
 
807
- /** @type {import('node:http2').ClientHttp2Stream} */
808
- let stream = null
809
-
810
891
  headers[HTTP2_HEADER_AUTHORITY] = host || client[kHostAuthority]
811
892
  headers[HTTP2_HEADER_METHOD] = method
812
893
 
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]()
894
+ // Single pre-shaped state object shared by all stream event handlers.
895
+ // All fields are declared up-front so the object keeps a stable hidden
896
+ // class for the whole request lifetime.
897
+ const state = {
898
+ abort: null,
899
+ body: request.body,
900
+ client,
901
+ contentLength: null,
902
+ expectsPayload: false,
903
+ request,
904
+ headersTimeout,
905
+ bodyTimeout,
906
+ requestFinalized: false,
907
+ responseReceived: false,
908
+ bodySent: false,
909
+ pendingEnd: false,
910
+ trailers: null,
911
+ session,
912
+ stream: null
823
913
  }
824
914
 
825
915
  const abort = (err, resetPendingIdx = false) => {
@@ -831,47 +921,39 @@ function writeH2 (client, request) {
831
921
 
832
922
  util.errorRequest(client, request, err)
833
923
 
834
- if (stream != null) {
924
+ if (state.stream != null) {
835
925
  clearRequestStream(request)
836
926
 
837
- // On Abort, we close the stream to send RST_STREAM frame
927
+ // On Abort, we close the stream to send RST_STREAM frame.
928
+ const stream = state.stream
838
929
  stream.close()
839
930
 
931
+ // close() alone leaves cleanup waiting on the 'close' event; on a busy,
932
+ // long-lived multiplexed session that event can fail to fire, leaving the
933
+ // native Http2Stream (and the whole request graph it pins) alive for the
934
+ // session's life. Destroy the stream synchronously to release the handle
935
+ // deterministically. Deferring the destroy (e.g. via setImmediate) leaks
936
+ // the same way when the event loop is stalled and the callback never runs
937
+ // under abort churn (#5558); close() has already queued the RST_STREAM
938
+ // frame on the native session, so a synchronous destroy still sends it.
939
+ if (!stream.destroyed) {
940
+ util.destroy(stream)
941
+ }
942
+
840
943
  // We move the running index to the next request
841
944
  client[kOnError](err)
842
- finalizeRequest(resetPendingIdx)
945
+ finalizeRequest(state, resetPendingIdx)
843
946
  }
844
947
 
845
948
  // We do not destroy the socket as we can continue using the session
846
949
  // the stream gets destroyed and the session remains to create new streams
847
- util.destroy(body, err)
950
+ util.destroy(state.body, err)
848
951
  }
849
952
 
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)
953
+ state.abort = abort
871
954
 
872
- return null
873
- }
874
- }
955
+ /** @type {import('node:http2').ClientHttp2Stream} */
956
+ let stream = null
875
957
 
876
958
  try {
877
959
  // We are already connected, streams are pending.
@@ -886,24 +968,13 @@ function writeH2 (client, request) {
886
968
  }
887
969
 
888
970
  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
- }
971
+ refH2Session(session)
901
972
 
902
973
  if (upgrade === 'websocket') {
903
974
  // We cannot upgrade to websocket if extended CONNECT protocol is not supported
904
975
  if (session[kEnableConnectProtocol] === false) {
905
976
  util.errorRequest(client, request, new InformationalError('HTTP/2: Extended CONNECT protocol not supported by server'))
906
- session.unref()
977
+ unrefH2Session(session)
907
978
  return false
908
979
  }
909
980
 
@@ -921,12 +992,12 @@ function writeH2 (client, request) {
921
992
  headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https'
922
993
  }
923
994
 
924
- stream = requestStream(headers, { endStream: false, signal })
995
+ stream = openStream(client, request, session, abort, headers, { endStream: false, signal })
925
996
  if (stream == null) {
926
- session.unref()
997
+ unrefH2Session(session)
927
998
  return false
928
999
  }
929
- setupUpgradeStream(stream, upgradeState)
1000
+ setupUpgradeStream(stream, state)
930
1001
  return true
931
1002
  }
932
1003
 
@@ -935,12 +1006,12 @@ function writeH2 (client, request) {
935
1006
  // will create a new stream. We trigger a request to create the stream and wait until
936
1007
  // `ready` event is triggered
937
1008
  // We disabled endStream to allow the user to write to the stream
938
- stream = requestStream(headers, { endStream: false, signal })
1009
+ stream = openStream(client, request, session, abort, headers, { endStream: false, signal })
939
1010
  if (stream == null) {
940
- session.unref()
1011
+ unrefH2Session(session)
941
1012
  return false
942
1013
  }
943
- setupUpgradeStream(stream, upgradeState)
1014
+ setupUpgradeStream(stream, state)
944
1015
 
945
1016
  return true
946
1017
  }
@@ -968,6 +1039,8 @@ function writeH2 (client, request) {
968
1039
  method === 'PROPPATCH'
969
1040
  )
970
1041
 
1042
+ let body = state.body
1043
+
971
1044
  if (body && typeof body.read === 'function') {
972
1045
  // Try to read EOF in order to get length.
973
1046
  body.read(0)
@@ -1014,7 +1087,7 @@ function writeH2 (client, request) {
1014
1087
  headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}`
1015
1088
  }
1016
1089
 
1017
- session.ref()
1090
+ refH2Session(session)
1018
1091
 
1019
1092
  if (channels.sendHeaders.hasSubscribers) {
1020
1093
  let header = ''
@@ -1026,26 +1099,16 @@ function writeH2 (client, request) {
1026
1099
 
1027
1100
  // TODO(metcoder95): add support for sending trailers
1028
1101
  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
- }
1102
+
1103
+ state.body = body
1104
+ state.contentLength = contentLength
1105
+ state.expectsPayload = expectsPayload
1043
1106
 
1044
1107
  if (expectContinue) {
1045
1108
  headers[HTTP2_HEADER_EXPECT] = '100-continue'
1046
1109
  }
1047
1110
 
1048
- stream = requestStream(headers, { endStream: shouldEndStream, signal })
1111
+ stream = openStream(client, request, session, abort, headers, { endStream: shouldEndStream, signal })
1049
1112
  if (stream == null) {
1050
1113
  return false
1051
1114
  }
@@ -1056,22 +1119,30 @@ function writeH2 (client, request) {
1056
1119
  // Increment counter as we have new streams open
1057
1120
  clearHttp2IdleTimeout(session)
1058
1121
  ++session[kOpenStreams]
1059
- stream.setTimeout(headersTimeout)
1122
+
1123
+ if (headersTimeout) {
1124
+ stream.setTimeout(headersTimeout)
1125
+ }
1060
1126
 
1061
1127
  stream[kHTTP2Session] = session
1062
- stream.once('close', onRequestStreamClose)
1128
+ stream.on('close', completeRequestStream)
1063
1129
 
1064
1130
  bindRequestToStream(request, stream, releaseRequestStream)
1065
1131
  if (expectContinue) {
1066
1132
  stream.once('continue', writeBodyH2)
1067
1133
  }
1068
- stream.once('response', onResponse)
1069
- stream.once('end', onEnd)
1070
- stream.once('error', onError)
1071
- stream.once('frameError', onFrameError)
1134
+ // The handlers below either remove themselves on first invocation or
1135
+ // become unreachable once the stream closes, so plain `on` avoids the
1136
+ // per-listener `once` wrapper allocation.
1137
+ stream.on('response', onResponse)
1138
+ stream.on('end', onEnd)
1139
+ stream.on('error', onError)
1140
+ stream.on('frameError', onFrameError)
1072
1141
  stream.on('aborted', onAborted)
1073
- stream.on('timeout', onTimeout)
1074
- stream.once('trailers', onTrailers)
1142
+ if (headersTimeout || bodyTimeout) {
1143
+ stream.on('timeout', onTimeout)
1144
+ }
1145
+ stream.on('trailers', onTrailers)
1075
1146
 
1076
1147
  if (!expectContinue) {
1077
1148
  writeBodyH2.call(stream)
@@ -1109,16 +1180,24 @@ function releaseRequestStream (stream) {
1109
1180
  detachRequestFromStream(request)
1110
1181
  }
1111
1182
 
1112
- removeRequestStreamListeners(stream)
1113
-
1183
+ // A closed or destroyed stream cannot emit further events; leaving the
1184
+ // listeners in place saves the removal scans (they are collected with
1185
+ // the stream). All handlers bail out when the stream state is gone.
1114
1186
  if (!stream.destroyed && !stream.closed) {
1187
+ removeRequestStreamListeners(stream)
1115
1188
  stream.once('error', noop)
1116
1189
  }
1117
1190
  }
1118
1191
 
1119
1192
  function onData (chunk) {
1120
1193
  const stream = this
1121
- const { request } = stream[kRequestStreamState]
1194
+ const state = stream[kRequestStreamState]
1195
+
1196
+ if (state == null) {
1197
+ return
1198
+ }
1199
+
1200
+ const { request } = state
1122
1201
 
1123
1202
  if (request.aborted || request.completed) {
1124
1203
  return
@@ -1132,22 +1211,40 @@ function onData (chunk) {
1132
1211
  function onResponse (headers) {
1133
1212
  const stream = this
1134
1213
  const state = stream[kRequestStreamState]
1214
+
1215
+ if (state == null) {
1216
+ return
1217
+ }
1218
+
1135
1219
  const { request } = state
1136
1220
 
1137
1221
  stream.off('response', onResponse)
1138
1222
 
1223
+ // Final response received while still awaiting 100 (Continue): the body won't
1224
+ // be sent, so close our half or the stream stays open and never completes.
1225
+ if (state.body != null && !state.bodySent && !stream.writableEnded) {
1226
+ stream.removeListener('continue', writeBodyH2)
1227
+ stream.end()
1228
+ }
1229
+
1139
1230
  const statusCode = headers[HTTP2_HEADER_STATUS]
1140
1231
  delete headers[HTTP2_HEADER_STATUS]
1141
1232
  request.onResponseStarted()
1142
1233
  state.responseReceived = true
1143
- stream.setTimeout(state.bodyTimeout)
1234
+
1235
+ if (state.headersTimeout || state.bodyTimeout) {
1236
+ stream.setTimeout(state.bodyTimeout)
1237
+ }
1144
1238
 
1145
1239
  // Due to the stream nature, it is possible we face a race condition
1146
1240
  // 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) {
1241
+ // or already completed and headers hasn't been received yet. A late
1242
+ // 'response' delivered after completion would call request.onResponseStart
1243
+ // post-completion, tripping its `assert(!this.completed)` (an uncatchable
1244
+ // throw on the http2 event tick). Guard `completed` here as onEnd/onTrailers
1245
+ // already do; best effort is to release the stream immediately as there's
1246
+ // no value to keep it open.
1247
+ if (request.aborted || request.completed) {
1151
1248
  releaseRequestStream(stream)
1152
1249
  return
1153
1250
  }
@@ -1162,17 +1259,25 @@ function onResponse (headers) {
1162
1259
  function onEnd () {
1163
1260
  const stream = this
1164
1261
  const state = stream[kRequestStreamState]
1262
+
1263
+ if (state == null) {
1264
+ return
1265
+ }
1266
+
1165
1267
  const { request } = state
1166
1268
 
1167
1269
  stream.off('end', onEnd)
1168
1270
 
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.
1271
+ // onTrailers (which may fire after 'end' on Windows) has already stored
1272
+ // trailers on the state by now, so completing here still delivers them.
1173
1273
  if (state.responseReceived) {
1174
1274
  if (!request.aborted && !request.completed) {
1175
1275
  state.pendingEnd = true
1276
+
1277
+ // Complete on 'end': a blocked event loop can keep the stream's 'close'
1278
+ // from firing, stranding its buffers until OOM. Idempotent, so a later
1279
+ // 'close' no-ops.
1280
+ completeRequestStream.call(stream)
1176
1281
  }
1177
1282
  } else {
1178
1283
  // Stream ended without receiving a response - this is an error
@@ -1185,6 +1290,10 @@ function onError (err) {
1185
1290
  const stream = this
1186
1291
  const state = stream[kRequestStreamState]
1187
1292
 
1293
+ if (state == null) {
1294
+ return
1295
+ }
1296
+
1188
1297
  stream.off('error', onError)
1189
1298
  state.abort(err)
1190
1299
  }
@@ -1193,6 +1302,10 @@ function onFrameError (type, code) {
1193
1302
  const stream = this
1194
1303
  const state = stream[kRequestStreamState]
1195
1304
 
1305
+ if (state == null) {
1306
+ return
1307
+ }
1308
+
1196
1309
  stream.off('frameError', onFrameError)
1197
1310
  state.abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
1198
1311
  }
@@ -1205,6 +1318,10 @@ function onTimeout () {
1205
1318
  const stream = this
1206
1319
  const state = stream[kRequestStreamState]
1207
1320
 
1321
+ if (state == null) {
1322
+ return
1323
+ }
1324
+
1208
1325
  // Remove self so timeout doesn't fire again after we handle it
1209
1326
  stream.off('timeout', onTimeout)
1210
1327
 
@@ -1217,6 +1334,11 @@ function onTimeout () {
1217
1334
  function onTrailers (trailers) {
1218
1335
  const stream = this
1219
1336
  const state = stream[kRequestStreamState]
1337
+
1338
+ if (state == null) {
1339
+ return
1340
+ }
1341
+
1220
1342
  const { request } = state
1221
1343
 
1222
1344
  stream.off('trailers', onTrailers)
@@ -1226,13 +1348,14 @@ function onTrailers (trailers) {
1226
1348
  return
1227
1349
  }
1228
1350
 
1229
- // Store trailers for onRequestStreamClose to use when completing
1351
+ // Store trailers for completeRequestStream to use when completing
1230
1352
  state.trailers = trailers
1231
1353
  }
1232
1354
 
1233
1355
  function writeBodyH2 () {
1234
1356
  const stream = this
1235
1357
  const state = stream[kRequestStreamState]
1358
+ state.bodySent = true
1236
1359
  const { abort, body, client, contentLength, expectsPayload, request } = state
1237
1360
 
1238
1361
  if (!body || contentLength === 0) {