@openclaw/discord 2026.6.8 → 2026.6.9-beta.1

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 (110) hide show
  1. package/dist/action-runtime-api.js +1 -1
  2. package/dist/api.js +13 -13
  3. package/dist/{approval-handler.runtime-B3gyUd-L.js → approval-handler.runtime-BprBDUQG.js} +3 -3
  4. package/dist/{audit-BlfewK04.js → audit-DuZUxGjM.js} +3 -3
  5. package/dist/{channel-b0hY1EJw.js → channel--3YhTUOb.js} +27 -19
  6. package/dist/{channel-actions-BnPHwCZ_.js → channel-actions-CoudAyAB.js} +12 -6
  7. package/dist/{channel-actions.runtime-DX5iW6ut.js → channel-actions.runtime-DdOfD3fi.js} +5 -5
  8. package/dist/channel-plugin-api.js +1 -1
  9. package/dist/{channel.setup-By5cfELZ.js → channel.setup-C-y6Jifd.js} +2 -2
  10. package/dist/{components-D-CYw0-b.js → components-CUmrNvv-.js} +1 -1
  11. package/dist/{conversation-identity-CKzQAqFF.js → conversation-identity-CN-HPn11.js} +2 -2
  12. package/dist/{directory-config-Bu7FYOsl.js → directory-config-BCt5KxcX.js} +1 -1
  13. package/dist/directory-contract-api.js +1 -1
  14. package/dist/{directory-live-LjENjK6L.js → directory-live-CE7IDmwo.js} +1 -1
  15. package/dist/{handle-action.guild-admin-BS2qnhXF.js → handle-action.guild-admin-B7SnVS0m.js} +14 -9
  16. package/dist/index.js +1 -1
  17. package/dist/{manager.runtime-BU1vkOeO.js → manager.runtime-zMVwNPAT.js} +12 -4
  18. package/dist/{message-handler-CQVkXHMN.js → message-handler-DE413Oj4.js} +6 -6
  19. package/dist/{message-handler.preflight-DAnLOeDA.js → message-handler.preflight-BuF-DsE2.js} +10 -10
  20. package/dist/{message-handler.process-Dp5NQT05.js → message-handler.process-BX8HsMfV.js} +37 -23
  21. package/dist/{message-utils-Bx993JLN.js → message-utils-s_8KDqAQ.js} +1 -1
  22. package/dist/{outbound-adapter-CmN7ao1t.js → outbound-adapter-CNievjXH.js} +6 -6
  23. package/dist/{pluralkit-SYmlmerw.js → pluralkit-BXkU9XmC.js} +1 -1
  24. package/dist/{provider-DrScDA1p.js → provider-ByZ6xxgi.js} +85 -56
  25. package/dist/{provider-session.runtime-DXTzSYOJ.js → provider-session.runtime-DMfaQ9Z6.js} +3 -3
  26. package/dist/provider.runtime-EW-G8l_U.js +2 -0
  27. package/dist/{resolve-channels-Rautpk8n.js → resolve-channels-t1URw0Qz.js} +1 -1
  28. package/dist/{resolve-users-Bw7vvtsi.js → resolve-users-Bapkb237.js} +1 -1
  29. package/dist/{runtime-C80YEJ7Z.js → runtime-C6jV3hf4.js} +24 -10
  30. package/dist/runtime-api.actions.js +2 -2
  31. package/dist/runtime-api.js +19 -19
  32. package/dist/runtime-api.lookup.js +4 -4
  33. package/dist/runtime-api.monitor-5BSxmucu.js +6 -0
  34. package/dist/runtime-api.monitor.js +4 -4
  35. package/dist/runtime-api.send.js +5 -5
  36. package/dist/runtime-api.threads.js +3 -3
  37. package/dist/{send-zGsXF-up.js → send-o-Y1DiAT.js} +3 -3
  38. package/dist/{send.components-ktzrUTkt.js → send.components-B4_oNcOh.js} +4 -4
  39. package/dist/{send.outbound-C8oC51um.js → send.outbound-DhiXV3UJ.js} +53 -10
  40. package/dist/{send.receipt-DsQWEQ2O.js → send.receipt-HXIwVvXy.js} +2 -1
  41. package/dist/{send.shared-Dvo2ZCVG.js → send.shared-DYdjs_Zh.js} +13 -5
  42. package/dist/session-binding-contract-api.js +1 -1
  43. package/dist/setup-plugin-api.js +1 -1
  44. package/dist/{subagent-hooks-kjrWDeDg.js → subagent-hooks-CbF_Z5F0.js} +2 -2
  45. package/dist/subagent-hooks-api.js +1 -1
  46. package/dist/{system-events-CvU3Aduf.js → system-events-BxTHlBbM.js} +1 -1
  47. package/dist/{target-resolver-DMPTzuo7.js → target-resolver-DpC8iueE.js} +2 -2
  48. package/dist/targets-BEIgHBHc.js +3 -0
  49. package/dist/{thread-bindings-DO32M2kW.js → thread-bindings-CEVvN75T.js} +4 -4
  50. package/dist/{thread-bindings.discord-api-304M1PMr.js → thread-bindings.discord-api-B8NfbxEB.js} +4 -4
  51. package/dist/{thread-bindings.manager-C9YT7wF2.js → thread-bindings.manager-BKfUaXGt.js} +3 -3
  52. package/dist/{transcripts-source-Chy2OrO_.js → transcripts-source-W6n_8J8g.js} +1 -1
  53. package/dist/transcripts-source-api.js +1 -1
  54. package/dist/{typing-BaivbXIG.js → typing-0-pUmlY9.js} +1 -1
  55. package/node_modules/undici/README.md +59 -18
  56. package/node_modules/undici/docs/docs/GettingStarted.md +278 -0
  57. package/node_modules/undici/docs/docs/api/Agent.md +3 -0
  58. package/node_modules/undici/docs/docs/api/BalancedPool.md +1 -1
  59. package/node_modules/undici/docs/docs/api/Client.md +44 -5
  60. package/node_modules/undici/docs/docs/api/Connector.md +1 -0
  61. package/node_modules/undici/docs/docs/api/Cookies.md +28 -1
  62. package/node_modules/undici/docs/docs/api/Dispatcher.md +22 -5
  63. package/node_modules/undici/docs/docs/api/EnvHttpProxyAgent.md +6 -9
  64. package/node_modules/undici/docs/docs/api/Errors.md +12 -0
  65. package/node_modules/undici/docs/docs/api/EventSource.md +50 -3
  66. package/node_modules/undici/docs/docs/api/Fetch.md +5 -3
  67. package/node_modules/undici/docs/docs/api/H2CClient.md +3 -3
  68. package/node_modules/undici/docs/docs/api/MockAgent.md +1 -1
  69. package/node_modules/undici/docs/docs/api/MockCallHistory.md +1 -1
  70. package/node_modules/undici/docs/docs/api/Pool.md +4 -1
  71. package/node_modules/undici/docs/docs/api/RedirectHandler.md +4 -1
  72. package/node_modules/undici/docs/docs/api/RetryAgent.md +3 -3
  73. package/node_modules/undici/docs/docs/api/RetryHandler.md +6 -6
  74. package/node_modules/undici/docs/docs/api/RoundRobinPool.md +1 -1
  75. package/node_modules/undici/docs/docs/api/SnapshotAgent.md +3 -3
  76. package/node_modules/undici/docs/docs/api/Socks5ProxyAgent.md +1 -0
  77. package/node_modules/undici/docs/docs/api/api-lifecycle.md +4 -4
  78. package/node_modules/undici/lib/core/connect.js +29 -4
  79. package/node_modules/undici/lib/core/util.js +8 -6
  80. package/node_modules/undici/lib/dispatcher/client-h1.js +69 -2
  81. package/node_modules/undici/lib/dispatcher/client-h2.js +160 -37
  82. package/node_modules/undici/lib/dispatcher/client.js +36 -7
  83. package/node_modules/undici/lib/dispatcher/dispatcher-base.js +1 -0
  84. package/node_modules/undici/lib/dispatcher/proxy-agent.js +2 -1
  85. package/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js +4 -2
  86. package/node_modules/undici/lib/handler/redirect-handler.js +36 -11
  87. package/node_modules/undici/lib/interceptor/dns.js +4 -0
  88. package/node_modules/undici/lib/interceptor/redirect.js +3 -3
  89. package/node_modules/undici/lib/mock/mock-call-history.js +1 -1
  90. package/node_modules/undici/lib/mock/snapshot-agent.js +9 -1
  91. package/node_modules/undici/lib/util/cache.js +8 -2
  92. package/node_modules/undici/lib/web/cookies/parse.js +17 -25
  93. package/node_modules/undici/lib/web/eventsource/eventsource.js +7 -18
  94. package/node_modules/undici/lib/web/eventsource/util.js +32 -1
  95. package/node_modules/undici/lib/web/fetch/body.js +43 -0
  96. package/node_modules/undici/lib/web/fetch/index.js +17 -3
  97. package/node_modules/undici/lib/web/fetch/request.js +33 -3
  98. package/node_modules/undici/lib/web/websocket/receiver.js +20 -3
  99. package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +8 -1
  100. package/node_modules/undici/lib/web/websocket/websocket.js +3 -1
  101. package/node_modules/undici/package.json +1 -1
  102. package/node_modules/undici/types/client.d.ts +5 -0
  103. package/node_modules/undici/types/connector.d.ts +1 -0
  104. package/node_modules/undici/types/fetch.d.ts +5 -1
  105. package/node_modules/undici/types/interceptors.d.ts +1 -1
  106. package/npm-shrinkwrap.json +7 -7
  107. package/package.json +5 -5
  108. package/dist/provider.runtime-nb-6cRoy.js +0 -2
  109. package/dist/runtime-api.monitor-D8KNDAd5.js +0 -6
  110. package/dist/targets-CKaNidbk.js +0 -3
@@ -8,7 +8,9 @@ const {
8
8
  RequestAbortedError,
9
9
  SocketError,
10
10
  InformationalError,
11
- InvalidArgumentError
11
+ InvalidArgumentError,
12
+ HeadersTimeoutError,
13
+ BodyTimeoutError
12
14
  } = require('../core/errors.js')
13
15
  const {
14
16
  kUrl,
@@ -33,6 +35,8 @@ const {
33
35
  kSize,
34
36
  kHTTPContext,
35
37
  kClosed,
38
+ kKeepAliveDefaultTimeout,
39
+ kHeadersTimeout,
36
40
  kBodyTimeout,
37
41
  kEnableConnectProtocol,
38
42
  kRemoteSettings,
@@ -81,6 +85,29 @@ function getGoAwayError (session, errorCode) {
81
85
  : new SocketError(`HTTP/2: "GOAWAY" frame received with code ${errorCode}`, util.getSocketInfo(session[kSocket])))
82
86
  }
83
87
 
88
+ function resetHttp2Session (session, err) {
89
+ const client = session[kClient]
90
+ const socket = session[kSocket]
91
+
92
+ if (client[kHTTP2Session] === session) {
93
+ client[kSocket] = null
94
+ client[kHTTPContext] = null
95
+ client[kHTTP2Session] = null
96
+ }
97
+
98
+ if (socket != null && socket[kError] == null) {
99
+ socket[kError] = err
100
+ }
101
+
102
+ if (!session.closed && !session.destroyed) {
103
+ try {
104
+ session.destroy(err)
105
+ } catch {}
106
+ }
107
+
108
+ util.destroy(socket, err)
109
+ }
110
+
84
111
  function getGoAwayPendingIdx (client, lastStreamID) {
85
112
  const maxAcceptedStreamID = Number.isInteger(lastStreamID) ? lastStreamID : Number.MAX_SAFE_INTEGER
86
113
 
@@ -122,6 +149,25 @@ function clearRequestStream (request) {
122
149
  cleanup?.(stream)
123
150
  }
124
151
 
152
+ function requeueUnsentRequest (client, request) {
153
+ client[kQueue].splice(client[kPendingIdx] + 1, 0, request)
154
+ }
155
+
156
+ function completeRequest (client, request, resetPendingIdx = false) {
157
+ const index = client[kQueue].indexOf(request, client[kRunningIdx])
158
+
159
+ if (index === -1 || index >= client[kPendingIdx]) {
160
+ return
161
+ }
162
+
163
+ client[kQueue].splice(index, 1)
164
+ client[kPendingIdx]--
165
+
166
+ if (resetPendingIdx && client[kPendingIdx] < client[kRunningIdx]) {
167
+ client[kPendingIdx] = client[kRunningIdx]
168
+ }
169
+ }
170
+
125
171
  function canRetryRequestAfterGoAway (request) {
126
172
  const { body } = request
127
173
 
@@ -161,6 +207,7 @@ function connectH2 (client, socket) {
161
207
  session[kClient] = client
162
208
  session[kSocket] = socket
163
209
  session[kHTTP2SessionState] = {
210
+ idleTimeout: null,
164
211
  ping: {
165
212
  interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
166
213
  }
@@ -249,10 +296,10 @@ function connectH2 (client, socket) {
249
296
  if (client[kRunning] > 0) {
250
297
  // We are already processing requests
251
298
 
252
- // Non-idempotent request cannot be retried.
253
- // Ensure that no other requests are inflight and
254
- // could cause failure.
255
- if (request.idempotent === false) return true
299
+ // Unlike HTTP/1.1 pipelining, HTTP/2 multiplexes requests on
300
+ // independent streams, so non-idempotent requests can be dispatched
301
+ // concurrently. Retry eligibility is handled by stream/session error
302
+ // handling instead of by serializing all non-idempotent requests.
256
303
  // Don't dispatch an upgrade until all preceding requests have completed.
257
304
  // Possibly, we do not have remote settings confirmed yet.
258
305
  if ((request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false) return true
@@ -278,16 +325,66 @@ function connectH2 (client, socket) {
278
325
 
279
326
  function resumeH2 (client) {
280
327
  const socket = client[kSocket]
328
+ const session = client[kHTTP2Session]
281
329
 
282
330
  if (socket?.destroyed === false) {
283
331
  if (client[kSize] === 0 || client[kMaxConcurrentStreams] === 0) {
284
332
  socket.unref()
285
- client[kHTTP2Session].unref()
333
+ session.unref()
286
334
  } else {
287
335
  socket.ref()
288
- client[kHTTP2Session].ref()
336
+ session.ref()
289
337
  }
338
+
339
+ if (client[kSize] === 0 && session[kOpenStreams] === 0) {
340
+ setHttp2IdleTimeout(session)
341
+ } else {
342
+ clearHttp2IdleTimeout(session)
343
+ }
344
+ }
345
+ }
346
+
347
+ function clearHttp2IdleTimeout (session) {
348
+ const state = session[kHTTP2SessionState]
349
+
350
+ if (state?.idleTimeout != null) {
351
+ clearTimeout(state.idleTimeout)
352
+ state.idleTimeout = null
353
+ }
354
+ }
355
+
356
+ function setHttp2IdleTimeout (session) {
357
+ const client = session[kClient]
358
+
359
+ if (client[kHTTP2Session] !== session || session.closed || session.destroyed) {
360
+ return
361
+ }
362
+
363
+ if (session[kOpenStreams] !== 0 || client[kSize] !== 0) {
364
+ clearHttp2IdleTimeout(session)
365
+ return
366
+ }
367
+
368
+ const state = session[kHTTP2SessionState]
369
+ if (state.idleTimeout == null) {
370
+ state.idleTimeout = setTimeout(onHttp2SessionIdleTimeout, client[kKeepAliveDefaultTimeout], session).unref()
371
+ }
372
+ }
373
+
374
+ function onHttp2SessionIdleTimeout (session) {
375
+ const client = session[kClient]
376
+ const socket = session[kSocket]
377
+ const state = session[kHTTP2SessionState]
378
+
379
+ state.idleTimeout = null
380
+
381
+ if (client[kHTTP2Session] !== session || session[kOpenStreams] !== 0 || client[kSize] !== 0 || session.closed || session.destroyed) {
382
+ return
290
383
  }
384
+
385
+ const err = new InformationalError('socket idle timeout')
386
+ socket[kError] = err
387
+ util.destroy(socket, err)
291
388
  }
292
389
 
293
390
  function applyConnectionWindowSize (connectionWindowSize) {
@@ -415,6 +512,8 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
415
512
  client[kHTTP2Session] = null
416
513
  }
417
514
 
515
+ clearHttp2IdleTimeout(this)
516
+
418
517
  if (!this.closed && !this.destroyed) {
419
518
  this.close()
420
519
  }
@@ -437,6 +536,8 @@ function onHttp2SessionClose () {
437
536
  client[kHTTP2Session] = null
438
537
  }
439
538
 
539
+ clearHttp2IdleTimeout(this)
540
+
440
541
  if (state.ping.interval != null) {
441
542
  clearInterval(state.ping.interval)
442
543
  state.ping.interval = null
@@ -449,7 +550,9 @@ function onHttp2SessionClose () {
449
550
  const requests = client[kQueue].splice(client[kRunningIdx])
450
551
  for (let i = 0; i < requests.length; i++) {
451
552
  const request = requests[i]
452
- util.errorRequest(client, request, err)
553
+ if (request != null) {
554
+ util.errorRequest(client, request, err)
555
+ }
453
556
  }
454
557
  }
455
558
  }
@@ -512,6 +615,7 @@ function closeStreamSession (stream) {
512
615
  session[kOpenStreams] -= 1
513
616
  if (session[kOpenStreams] === 0) {
514
617
  session.unref()
618
+ setHttp2IdleTimeout(session)
515
619
  }
516
620
  }
517
621
 
@@ -526,6 +630,19 @@ function onUpgradeStreamClose () {
526
630
  }
527
631
 
528
632
  function onRequestStreamClose () {
633
+ const state = this[kRequestStreamState]
634
+
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)
639
+
640
+ if (state.pendingEnd && !state.request.aborted && !state.request.completed) {
641
+ state.request.onResponseEnd(state.trailers || {})
642
+ state.finalizeRequest()
643
+ }
644
+ }
645
+
529
646
  this.off('data', onData)
530
647
  this.off('error', noop)
531
648
  closeStreamSession(this)
@@ -629,7 +746,7 @@ function onUpgradeStreamEnd () {
629
746
 
630
747
  function onUpgradeStreamTimeout () {
631
748
  const state = this[kRequestStreamState]
632
- failUpgradeStream(state, new InformationalError(`HTTP/2: "stream timeout after ${state.requestTimeout}"`))
749
+ failUpgradeStream(state, new InformationalError(`HTTP/2: "stream timeout after ${state.headersTimeout}"`))
633
750
  }
634
751
 
635
752
  function onUpgradeResponse (headers, _flags) {
@@ -654,7 +771,7 @@ function onUpgradeResponse (headers, _flags) {
654
771
  }
655
772
 
656
773
  function setupUpgradeStream (stream, state) {
657
- const { request, requestTimeout, session } = state
774
+ const { request, headersTimeout, session } = state
658
775
 
659
776
  stream[kHTTP2Stream] = true
660
777
  stream[kHTTP2Session] = session
@@ -668,12 +785,14 @@ function setupUpgradeStream (stream, state) {
668
785
  stream.on('timeout', onUpgradeStreamTimeout)
669
786
  stream.once('close', onUpgradeStreamClose)
670
787
 
788
+ clearHttp2IdleTimeout(session)
671
789
  ++session[kOpenStreams]
672
- stream.setTimeout(requestTimeout)
790
+ stream.setTimeout(headersTimeout)
673
791
  }
674
792
 
675
793
  function writeH2 (client, request) {
676
- const requestTimeout = request.bodyTimeout ?? client[kBodyTimeout]
794
+ const headersTimeout = request.headersTimeout ?? client[kHeadersTimeout]
795
+ const bodyTimeout = request.bodyTimeout ?? client[kBodyTimeout]
677
796
  const session = client[kHTTP2Session]
678
797
  const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
679
798
  let { body } = request
@@ -698,11 +817,7 @@ function writeH2 (client, request) {
698
817
  }
699
818
 
700
819
  requestFinalized = true
701
- client[kQueue][client[kRunningIdx]++] = null
702
-
703
- if (resetPendingIdx) {
704
- client[kPendingIdx] = client[kRunningIdx]
705
- }
820
+ completeRequest(client, request, resetPendingIdx)
706
821
 
707
822
  client[kResume]()
708
823
  }
@@ -736,8 +851,14 @@ function writeH2 (client, request) {
736
851
  try {
737
852
  return session.request(headers, options)
738
853
  } catch (err) {
739
- if (err?.code !== 'ERR_HTTP2_INVALID_CONNECTION_HEADERS') {
740
- throw 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
741
862
  }
742
863
 
743
864
  const wrappedErr = new InformationalError(err.message, { cause: err })
@@ -771,7 +892,8 @@ function writeH2 (client, request) {
771
892
  abort,
772
893
  finalizeRequest,
773
894
  request,
774
- requestTimeout,
895
+ headersTimeout,
896
+ bodyTimeout,
775
897
  responseReceived: false,
776
898
  session,
777
899
  stream: null
@@ -912,7 +1034,8 @@ function writeH2 (client, request) {
912
1034
  expectsPayload,
913
1035
  finalizeRequest,
914
1036
  request,
915
- requestTimeout,
1037
+ headersTimeout,
1038
+ bodyTimeout,
916
1039
  responseReceived: false,
917
1040
  session,
918
1041
  stream: null
@@ -929,11 +1052,11 @@ function writeH2 (client, request) {
929
1052
  stream[kHTTP2Stream] = true
930
1053
  stream[kRequestStreamState] = state
931
1054
  state.stream = stream
932
- bindRequestToStream(request, stream, null)
933
1055
 
934
1056
  // Increment counter as we have new streams open
1057
+ clearHttp2IdleTimeout(session)
935
1058
  ++session[kOpenStreams]
936
- stream.setTimeout(requestTimeout)
1059
+ stream.setTimeout(headersTimeout)
937
1060
 
938
1061
  stream[kHTTP2Session] = session
939
1062
  stream.once('close', onRequestStreamClose)
@@ -1017,6 +1140,7 @@ function onResponse (headers) {
1017
1140
  delete headers[HTTP2_HEADER_STATUS]
1018
1141
  request.onResponseStarted()
1019
1142
  state.responseReceived = true
1143
+ stream.setTimeout(state.bodyTimeout)
1020
1144
 
1021
1145
  // Due to the stream nature, it is possible we face a race condition
1022
1146
  // where the stream has been assigned, but the request has been aborted
@@ -1042,14 +1166,14 @@ function onEnd () {
1042
1166
 
1043
1167
  stream.off('end', onEnd)
1044
1168
 
1045
- releaseRequestStream(stream)
1046
- // If we received a response, this is a normal completion
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.
1047
1173
  if (state.responseReceived) {
1048
1174
  if (!request.aborted && !request.completed) {
1049
- request.onResponseEnd({})
1175
+ state.pendingEnd = true
1050
1176
  }
1051
-
1052
- state.finalizeRequest()
1053
1177
  } else {
1054
1178
  // Stream ended without receiving a response - this is an error
1055
1179
  // (e.g., server destroyed the stream before sending headers)
@@ -1062,8 +1186,6 @@ function onError (err) {
1062
1186
  const state = stream[kRequestStreamState]
1063
1187
 
1064
1188
  stream.off('error', onError)
1065
-
1066
- releaseRequestStream(stream)
1067
1189
  state.abort(err)
1068
1190
  }
1069
1191
 
@@ -1072,8 +1194,6 @@ function onFrameError (type, code) {
1072
1194
  const state = stream[kRequestStreamState]
1073
1195
 
1074
1196
  stream.off('frameError', onFrameError)
1075
-
1076
- releaseRequestStream(stream)
1077
1197
  state.abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
1078
1198
  }
1079
1199
 
@@ -1085,9 +1205,12 @@ function onTimeout () {
1085
1205
  const stream = this
1086
1206
  const state = stream[kRequestStreamState]
1087
1207
 
1088
- releaseRequestStream(stream)
1208
+ // Remove self so timeout doesn't fire again after we handle it
1209
+ stream.off('timeout', onTimeout)
1089
1210
 
1090
- const err = new InformationalError(`HTTP/2: "stream timeout after ${state.requestTimeout}"`)
1211
+ const err = state.responseReceived
1212
+ ? new BodyTimeoutError(`HTTP/2: "stream timeout after ${state.bodyTimeout}"`)
1213
+ : new HeadersTimeoutError(`HTTP/2: "headers timeout after ${state.headersTimeout}"`)
1091
1214
  state.abort(err)
1092
1215
  }
1093
1216
 
@@ -1097,14 +1220,14 @@ function onTrailers (trailers) {
1097
1220
  const { request } = state
1098
1221
 
1099
1222
  stream.off('trailers', onTrailers)
1223
+ stream.off('data', onData)
1100
1224
 
1101
1225
  if (request.aborted || request.completed) {
1102
1226
  return
1103
1227
  }
1104
1228
 
1105
- releaseRequestStream(stream)
1106
- request.onResponseEnd(trailers)
1107
- state.finalizeRequest()
1229
+ // Store trailers for onRequestStreamClose to use when completing
1230
+ state.trailers = trailers
1108
1231
  }
1109
1232
 
1110
1233
  function writeBodyH2 () {
@@ -76,6 +76,18 @@ function getPipelining (client) {
76
76
  return client[kPipelining] ?? client[kHTTPContext]?.defaultPipelining ?? 1
77
77
  }
78
78
 
79
+ // Protocol-aware dispatch ceiling. h1 RFC7230 pipelining is unrelated to h2
80
+ // stream multiplexing — over h2 the ceiling is the (server-confirmed)
81
+ // maxConcurrentStreams. Before a context is attached we use the h1
82
+ // pipelining factor; once h2 attaches the queued requests can drain in
83
+ // one batch up to maxConcurrentStreams.
84
+ function getMaxConcurrent (client) {
85
+ if (client[kHTTPContext]?.version === 'h2') {
86
+ return client[kMaxConcurrentStreams]
87
+ }
88
+ return getPipelining(client)
89
+ }
90
+
79
91
  /**
80
92
  * @type {import('../../types/client.js').default}
81
93
  */
@@ -326,10 +338,17 @@ class Client extends DispatcherBase {
326
338
  }
327
339
 
328
340
  get [kBusy] () {
341
+ // The `kPending > 0` check below is the gate Pool uses to decide whether
342
+ // to spin up an additional Client. For h1 that fan-out is correct —
343
+ // each socket only handles one pipelined request at a time. Once an h2
344
+ // context is attached we want concurrent dispatches to multiplex onto
345
+ // the shared session, so suppress that signal in the h2 case.
346
+ const allowsMux = this[kHTTPContext]?.version === 'h2'
347
+
329
348
  return Boolean(
330
349
  this[kHTTPContext]?.busy(null) ||
331
- (this[kSize] >= (getPipelining(this) || 1)) ||
332
- this[kPending] > 0
350
+ (this[kSize] >= (getMaxConcurrent(this) || 1)) ||
351
+ (this[kPending] > 0 && !allowsMux)
333
352
  )
334
353
  }
335
354
 
@@ -376,7 +395,9 @@ class Client extends DispatcherBase {
376
395
  const requests = this[kQueue].splice(this[kPendingIdx])
377
396
  for (let i = 0; i < requests.length; i++) {
378
397
  const request = requests[i]
379
- util.errorRequest(this, request, err)
398
+ if (request != null) {
399
+ util.errorRequest(this, request, err)
400
+ }
380
401
  }
381
402
 
382
403
  const callback = () => {
@@ -415,7 +436,9 @@ function onError (client, err) {
415
436
 
416
437
  for (let i = 0; i < requests.length; i++) {
417
438
  const request = requests[i]
418
- util.errorRequest(client, request, err)
439
+ if (request != null) {
440
+ util.errorRequest(client, request, err)
441
+ }
419
442
  }
420
443
  assert(client[kSize] === 0)
421
444
  }
@@ -549,9 +572,15 @@ function handleConnectError (client, err, { host, hostname, protocol, port }) {
549
572
  }
550
573
 
551
574
  if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
552
- assert(client[kRunning] === 0)
575
+ const running = client[kQueue].splice(client[kRunningIdx], client[kRunning])
576
+ client[kPendingIdx] = client[kRunningIdx]
577
+
578
+ for (let i = 0; i < running.length; i++) {
579
+ util.errorRequest(client, running[i], err)
580
+ }
581
+
553
582
  while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) {
554
- const request = client[kQueue][client[kPendingIdx]++]
583
+ const request = client[kQueue].splice(client[kPendingIdx], 1)[0]
555
584
  util.errorRequest(client, request, err)
556
585
  }
557
586
  } else {
@@ -616,7 +645,7 @@ function _resume (client, sync) {
616
645
  return
617
646
  }
618
647
 
619
- if (client[kRunning] >= (getPipelining(client) || 1)) {
648
+ if (client[kRunning] >= (getMaxConcurrent(client) || 1)) {
620
649
  return
621
650
  }
622
651
 
@@ -38,6 +38,7 @@ class DispatcherBase extends Dispatcher {
38
38
  */
39
39
  get webSocketOptions () {
40
40
  return {
41
+ maxFragments: this[kWebSocketOptions].maxFragments ?? 131072,
41
42
  maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 // 128 MB default
42
43
  }
43
44
  }
@@ -147,7 +147,8 @@ class ProxyAgent extends DispatcherBase {
147
147
  factory: agentFactory,
148
148
  username: opts.username || username,
149
149
  password: opts.password || password,
150
- proxyTls: opts.proxyTls
150
+ proxyTls: opts.proxyTls,
151
+ requestTls: opts.requestTls
151
152
  })
152
153
  }
153
154
 
@@ -19,6 +19,7 @@ const kProxyAuth = Symbol('proxy auth')
19
19
  const kProxyProtocol = Symbol('proxy protocol')
20
20
  const kPools = Symbol('pools')
21
21
  const kConnector = Symbol('connector')
22
+ const kRequestTls = Symbol('request tls settings')
22
23
 
23
24
  // Static flag to ensure warning is only emitted once per process
24
25
  let experimentalWarningEmitted = false
@@ -53,6 +54,7 @@ class Socks5ProxyAgent extends DispatcherBase {
53
54
  this[kProxyUrl] = url
54
55
  this[kProxyHeaders] = options.headers || {}
55
56
  this[kProxyProtocol] = options.proxyTls ? 'https:' : 'http:'
57
+ this[kRequestTls] = options.requestTls
56
58
 
57
59
  // Extract auth from URL or options
58
60
  this[kProxyAuth] = {
@@ -205,9 +207,9 @@ class Socks5ProxyAgent extends DispatcherBase {
205
207
  }
206
208
  debug('upgrading to TLS')
207
209
  finalSocket = tls.connect({
210
+ ...this[kRequestTls],
208
211
  socket,
209
- servername: targetHost,
210
- ...connectOpts.tls || {}
212
+ servername: this[kRequestTls]?.servername || targetHost
211
213
  })
212
214
 
213
215
  const tlsReady = Promise.withResolvers()
@@ -29,9 +29,11 @@ class RedirectHandler {
29
29
 
30
30
  this.dispatch = dispatch
31
31
  this.location = null
32
- const { maxRedirections: _, ...cleanOpts } = opts
32
+ const { maxRedirections: _, stripHeadersOnRedirect, stripHeadersOnCrossOriginRedirect, ...cleanOpts } = opts
33
33
  this.opts = cleanOpts // opts must be a copy, exclude maxRedirections
34
34
  this.opts.body = util.wrapRequestBody(this.opts.body)
35
+ this.stripHeadersOnRedirect = normalizeStripHeaders(stripHeadersOnRedirect, 'stripHeadersOnRedirect')
36
+ this.stripHeadersOnCrossOriginRedirect = normalizeStripHeaders(stripHeadersOnCrossOriginRedirect, 'stripHeadersOnCrossOriginRedirect')
35
37
  this.maxRedirections = maxRedirections
36
38
  this.handler = handler
37
39
  this.history = []
@@ -100,7 +102,7 @@ class RedirectHandler {
100
102
  // Remove headers referring to the original URL.
101
103
  // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers.
102
104
  // https://tools.ietf.org/html/rfc7231#section-6.4
103
- this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin)
105
+ this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin, this.stripHeadersOnRedirect, this.stripHeadersOnCrossOriginRedirect)
104
106
  this.opts.path = path
105
107
  this.opts.origin = origin
106
108
  this.opts.query = null
@@ -152,26 +154,49 @@ class RedirectHandler {
152
154
  }
153
155
 
154
156
  // https://tools.ietf.org/html/rfc7231#section-6.4.4
155
- function shouldRemoveHeader (header, removeContent, unknownOrigin) {
156
- if (header.length === 4) {
157
- return util.headerNameToString(header) === 'host'
157
+ function shouldRemoveHeader (header, removeContent, unknownOrigin, stripHeaders, stripHeadersOnCrossOrigin) {
158
+ const name = util.headerNameToString(header)
159
+ if (name === 'host') {
160
+ return true
161
+ }
162
+ if (stripHeaders?.has(name) || (unknownOrigin && stripHeadersOnCrossOrigin?.has(name))) {
163
+ return true
158
164
  }
159
- if (removeContent && util.headerNameToString(header).startsWith('content-')) {
165
+ if (removeContent && name.startsWith('content-')) {
160
166
  return true
161
167
  }
162
- if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) {
163
- const name = util.headerNameToString(header)
168
+ if (unknownOrigin) {
164
169
  return name === 'authorization' || name === 'cookie' || name === 'proxy-authorization'
165
170
  }
166
171
  return false
167
172
  }
168
173
 
169
174
  // https://tools.ietf.org/html/rfc7231#section-6.4
170
- function cleanRequestHeaders (headers, removeContent, unknownOrigin) {
175
+ function normalizeStripHeaders (headers, optionName) {
176
+ if (headers == null) {
177
+ return null
178
+ }
179
+
180
+ if (!Array.isArray(headers)) {
181
+ throw new InvalidArgumentError(`${optionName} must be an array`)
182
+ }
183
+
184
+ const normalized = new Set()
185
+ for (const header of headers) {
186
+ if (typeof header !== 'string') {
187
+ throw new InvalidArgumentError(`${optionName} must contain header names`)
188
+ }
189
+
190
+ normalized.add(util.headerNameToString(header))
191
+ }
192
+ return normalized
193
+ }
194
+
195
+ function cleanRequestHeaders (headers, removeContent, unknownOrigin, stripHeaders, stripHeadersOnCrossOrigin) {
171
196
  const ret = []
172
197
  if (Array.isArray(headers)) {
173
198
  for (let i = 0; i < headers.length; i += 2) {
174
- if (!shouldRemoveHeader(headers[i], removeContent, unknownOrigin)) {
199
+ if (!shouldRemoveHeader(headers[i], removeContent, unknownOrigin, stripHeaders, stripHeadersOnCrossOrigin)) {
175
200
  ret.push(headers[i], headers[i + 1])
176
201
  }
177
202
  }
@@ -179,7 +204,7 @@ function cleanRequestHeaders (headers, removeContent, unknownOrigin) {
179
204
  const entries = util.hasSafeIterator(headers) ? headers : Object.entries(headers)
180
205
 
181
206
  for (const [key, value] of entries) {
182
- if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) {
207
+ if (!shouldRemoveHeader(key, removeContent, unknownOrigin, stripHeaders, stripHeadersOnCrossOrigin)) {
183
208
  ret.push(key, value)
184
209
  }
185
210
  }
@@ -535,6 +535,10 @@ module.exports = interceptorOpts => {
535
535
 
536
536
  return dispatch => {
537
537
  return function dnsInterceptor (origDispatchOpts, handler) {
538
+ if (origDispatchOpts.origin == null) {
539
+ return dispatch(origDispatchOpts, handler)
540
+ }
541
+
538
542
  const origin =
539
543
  origDispatchOpts.origin.constructor === URL
540
544
  ? origDispatchOpts.origin
@@ -2,16 +2,16 @@
2
2
 
3
3
  const RedirectHandler = require('../handler/redirect-handler')
4
4
 
5
- function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections, throwOnMaxRedirect: defaultThrowOnMaxRedirect } = {}) {
5
+ function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections, throwOnMaxRedirect: defaultThrowOnMaxRedirect, stripHeadersOnRedirect: defaultStripHeadersOnRedirect, stripHeadersOnCrossOriginRedirect: defaultStripHeadersOnCrossOriginRedirect } = {}) {
6
6
  return (dispatch) => {
7
7
  return function Intercept (opts, handler) {
8
- const { maxRedirections = defaultMaxRedirections, throwOnMaxRedirect = defaultThrowOnMaxRedirect, ...rest } = opts
8
+ const { maxRedirections = defaultMaxRedirections, throwOnMaxRedirect = defaultThrowOnMaxRedirect, stripHeadersOnRedirect = defaultStripHeadersOnRedirect, stripHeadersOnCrossOriginRedirect = defaultStripHeadersOnCrossOriginRedirect, ...rest } = opts
9
9
 
10
10
  if (maxRedirections == null || maxRedirections === 0) {
11
11
  return dispatch(opts, handler)
12
12
  }
13
13
 
14
- const dispatchOpts = { ...rest, throwOnMaxRedirect } // Stop sub dispatcher from also redirecting.
14
+ const dispatchOpts = { ...rest, throwOnMaxRedirect, stripHeadersOnRedirect, stripHeadersOnCrossOriginRedirect } // Stop sub dispatcher from also redirecting.
15
15
  const redirectHandler = new RedirectHandler(dispatch, maxRedirections, dispatchOpts, handler)
16
16
  return dispatch(dispatchOpts, redirectHandler)
17
17
  }
@@ -35,7 +35,7 @@ function buildAndValidateFilterCallsOptions (options = {}) {
35
35
  }
36
36
 
37
37
  function makeFilterCalls (parameterName) {
38
- return (parameterValue, logs) => {
38
+ return (parameterValue, logs = this.logs) => {
39
39
  if (typeof parameterValue === 'string' || parameterValue == null) {
40
40
  return logs.filter((log) => {
41
41
  return log[parameterName] === parameterValue
@@ -354,7 +354,15 @@ class SnapshotAgent extends MockAgent {
354
354
  * @returns {Promise<void>}
355
355
  */
356
356
  async close () {
357
- await this[kSnapshotRecorder].close()
357
+ // In playback mode the recorder must not persist to disk. findSnapshot()
358
+ // mutates each matched snapshot's callCount, so saving on close would
359
+ // rewrite the snapshot file even though nothing new was recorded. Only
360
+ // record/update modes should write snapshots; playback just cleans up.
361
+ if (this[kSnapshotMode] === 'playback') {
362
+ this[kSnapshotRecorder].destroy()
363
+ } else {
364
+ await this[kSnapshotRecorder].close()
365
+ }
358
366
  await this[kRealAgent]?.close()
359
367
  await super.close()
360
368
  }