@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
@@ -3,6 +3,7 @@
3
3
  const util = require('../core/util')
4
4
  const assert = require('node:assert')
5
5
  const { InvalidArgumentError } = require('../core/errors')
6
+ const { kRequestOrigin } = require('../core/symbols')
6
7
 
7
8
  const redirectableStatusCodes = [300, 301, 302, 303, 307, 308]
8
9
 
@@ -43,6 +44,14 @@ class RedirectHandler {
43
44
  this.handler.onRequestStart?.(controller, { ...context, history: this.history })
44
45
  }
45
46
 
47
+ onBodySent (chunk) {
48
+ this.handler.onBodySent?.(chunk)
49
+ }
50
+
51
+ onRequestSent () {
52
+ this.handler.onRequestSent?.()
53
+ }
54
+
46
55
  onRequestUpgrade (controller, statusCode, headers, socket) {
47
56
  this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
48
57
  }
@@ -52,15 +61,19 @@ class RedirectHandler {
52
61
  throw new Error('max redirects')
53
62
  }
54
63
 
64
+ let removeContentHeaders = statusCode === 303
65
+
55
66
  // https://tools.ietf.org/html/rfc7231#section-6.4.2
56
67
  // https://fetch.spec.whatwg.org/#http-redirect-fetch
57
68
  // In case of HTTP 301 or 302 with POST, change the method to GET
69
+ // QUERY is safe (RFC 10008) and should not change method like GET.
58
70
  if ((statusCode === 301 || statusCode === 302) && this.opts.method === 'POST') {
59
71
  this.opts.method = 'GET'
60
72
  if (util.isStream(this.opts.body)) {
61
73
  util.destroy(this.opts.body.on('error', noop))
62
74
  }
63
75
  this.opts.body = null
76
+ removeContentHeaders = true
64
77
  }
65
78
 
66
79
  // https://tools.ietf.org/html/rfc7231#section-6.4.4
@@ -77,8 +90,12 @@ class RedirectHandler {
77
90
  ? null
78
91
  : headers.location
79
92
 
80
- if (this.opts.origin) {
81
- this.history.push(new URL(this.opts.path, this.opts.origin))
93
+ const requestOrigin = this.opts[kRequestOrigin] === undefined
94
+ ? this.opts.origin
95
+ : this.opts[kRequestOrigin]
96
+
97
+ if (requestOrigin) {
98
+ this.history.push(new URL(this.opts.path, requestOrigin))
82
99
  }
83
100
 
84
101
  if (!this.location) {
@@ -86,7 +103,10 @@ class RedirectHandler {
86
103
  return
87
104
  }
88
105
 
89
- const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
106
+ const baseUrl = requestOrigin
107
+ ? new URL(this.opts.path, requestOrigin)
108
+ : undefined
109
+ const { origin, pathname, search } = util.parseURL(new URL(this.location, baseUrl))
90
110
  const path = search ? `${pathname}${search}` : pathname
91
111
 
92
112
  // Check for redirect loops by seeing if we've already visited this URL in our history
@@ -100,11 +120,12 @@ class RedirectHandler {
100
120
  }
101
121
 
102
122
  // Remove headers referring to the original URL.
103
- // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers.
123
+ // By default it is Host only. A 303 or a 301/302 POST-to-GET redirect also removes all Content-* headers.
104
124
  // https://tools.ietf.org/html/rfc7231#section-6.4
105
- this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin, this.stripHeadersOnRedirect, this.stripHeadersOnCrossOriginRedirect)
125
+ this.opts.headers = cleanRequestHeaders(this.opts.headers, removeContentHeaders, requestOrigin !== origin, this.stripHeadersOnRedirect, this.stripHeadersOnCrossOriginRedirect)
106
126
  this.opts.path = path
107
127
  this.opts.origin = origin
128
+ this.opts[kRequestOrigin] = origin
108
129
  this.opts.query = null
109
130
  }
110
131
 
@@ -2,7 +2,7 @@
2
2
  const assert = require('node:assert')
3
3
 
4
4
  const { kRetryHandlerDefaultRetry } = require('../core/symbols')
5
- const { RequestRetryError } = require('../core/errors')
5
+ const { RequestRetryError, RequestAbortedError } = require('../core/errors')
6
6
  const {
7
7
  isDisturbed,
8
8
  parseRangeHeader,
@@ -11,7 +11,72 @@ const {
11
11
 
12
12
  function calculateRetryAfterHeader (retryAfter) {
13
13
  const retryTime = new Date(retryAfter).getTime()
14
- return isNaN(retryTime) ? 0 : retryTime - Date.now()
14
+ return isNaN(retryTime) ? null : retryTime - Date.now()
15
+ }
16
+
17
+ function validatePartialResponseContentLength (headers, range, statusCode, retryCount) {
18
+ const contentLength = headers['content-length']
19
+ if (contentLength == null) {
20
+ return
21
+ }
22
+
23
+ if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) {
24
+ return
25
+ }
26
+
27
+ const length = Number(contentLength)
28
+ const expectedLength = range.end - range.start + 1
29
+ if (!Number.isFinite(length) || length !== expectedLength) {
30
+ throw new RequestRetryError('Content-Length mismatch', statusCode, {
31
+ headers,
32
+ data: { count: retryCount }
33
+ })
34
+ }
35
+ }
36
+
37
+ // A stable controller handed to the downstream handler for the lifetime of the
38
+ // request. Each transparent retry/resume is a *separate* dispatch with its
39
+ // *own* connection controller. Without a stable proxy the downstream body keeps
40
+ // flow-controlling the original (now-dead) controller while data flows on the
41
+ // new one: backpressure pauses the new connection's controller, but the
42
+ // consumer's resume() targets the old one, so the resumed body stalls forever.
43
+ // The proxy always forwards to the controller of the currently active connection.
44
+ // An abort is additionally reported to the handler so it can cancel a pending
45
+ // retry backoff instead of letting the request hang until the backoff elapses.
46
+ // The notification is a private callback the handler hands over on construction,
47
+ // so nothing outside the handler can trigger it.
48
+ class RetryController {
49
+ #onAbort
50
+
51
+ constructor (onAbort) {
52
+ this.#onAbort = onAbort
53
+ this.target = null
54
+ }
55
+
56
+ pause () { this.target?.pause() }
57
+ resume () { this.target?.resume() }
58
+
59
+ abort (reason) {
60
+ this.target?.abort(reason)
61
+ this.#onAbort(reason)
62
+ }
63
+
64
+ get paused () { return this.target?.paused ?? false }
65
+ get aborted () { return this.target?.aborted ?? false }
66
+ get reason () { return this.target?.reason ?? null }
67
+ get rawHeaders () { return this.target?.rawHeaders ?? null }
68
+ set rawHeaders (value) {
69
+ if (this.target) {
70
+ this.target.rawHeaders = value
71
+ }
72
+ }
73
+
74
+ get rawTrailers () { return this.target?.rawTrailers ?? null }
75
+ set rawTrailers (value) {
76
+ if (this.target) {
77
+ this.target.rawTrailers = value
78
+ }
79
+ }
15
80
  }
16
81
 
17
82
  class RetryHandler {
@@ -45,7 +110,7 @@ class RetryHandler {
45
110
  timeoutFactor: timeoutFactor ?? 2,
46
111
  maxRetries: maxRetries ?? 5,
47
112
  // What errors we should retry
48
- methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'],
113
+ methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE', 'QUERY'],
49
114
  // Indicates which errors to retry
50
115
  statusCodes: statusCodes ?? [500, 502, 503, 504, 429],
51
116
  // List of errors to retry
@@ -70,14 +135,32 @@ class RetryHandler {
70
135
  this.etag = null
71
136
  this.statusCode = null
72
137
  this.headers = null
138
+ this.controllerProxy = new RetryController(reason => this.#onAbort(reason))
139
+ // A retry decision is in flight (the policy may be holding a backoff
140
+ // timer). While pending, a consumer abort cancels the wait.
141
+ this.retryPending = false
142
+ // Backoff timer returned by the retry policy, so #onAbort can cancel it.
143
+ // Null for custom policies that do not return their timer.
144
+ this.retryTimer = null
145
+ // Set once an abort during the backoff delivered the terminal error
146
+ // downstream; late policy callbacks and connection errors are then moot.
147
+ this.aborted = false
73
148
  }
74
149
 
75
150
  onResponseStartWithRetry (controller, statusCode, headers, statusMessage, err) {
76
151
  if (this.retryOpts.throwOnError) {
77
152
  // Preserve old behavior for status codes that are not eligible for retry
78
153
  if (this.retryOpts.statusCodes.includes(statusCode) === false) {
79
- this.headersSent = true
80
- this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
154
+ if (this.headersSent) {
155
+ // The downstream handler already received the response from an
156
+ // earlier attempt. Forwarding this response would replace the
157
+ // downstream body and leave the original body pending forever.
158
+ this.handler.onResponseError?.(this.controllerProxy, err)
159
+ } else {
160
+ this.headersSent = true
161
+ this.checkpointResponseEnd(headers)
162
+ this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
163
+ }
81
164
  } else {
82
165
  this.error = err
83
166
  }
@@ -87,14 +170,30 @@ class RetryHandler {
87
170
 
88
171
  if (isDisturbed(this.opts.body)) {
89
172
  this.headersSent = true
90
- this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
173
+ this.checkpointResponseEnd(headers)
174
+ this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
91
175
  return
92
176
  }
93
177
 
94
178
  function shouldRetry (passedErr) {
179
+ if (this.aborted) {
180
+ // Aborted while the policy was deciding; the decision is moot.
181
+ return
182
+ }
183
+ this.retryPending = false
184
+ this.retryTimer = null
185
+
95
186
  if (passedErr) {
96
- this.headersSent = true
97
- this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
187
+ if (this.headersSent) {
188
+ // The downstream handler already received the response from an
189
+ // earlier attempt. Forwarding this response would replace the
190
+ // downstream body and leave the original body pending forever.
191
+ this.handler.onResponseError?.(this.controllerProxy, passedErr)
192
+ } else {
193
+ this.headersSent = true
194
+ this.checkpointResponseEnd(headers)
195
+ this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
196
+ }
98
197
  controller.resume()
99
198
  return
100
199
  }
@@ -103,25 +202,63 @@ class RetryHandler {
103
202
  controller.resume()
104
203
  }
105
204
 
205
+ // The pause()/resume() pair (here and in shouldRetry) acts on THIS
206
+ // connection's controller -- never the downstream proxy. We hold this exact
207
+ // connection while the retry policy decides (possibly after a timeout) and
208
+ // must resume the same one. Routing this through controllerProxy would risk
209
+ // resuming a different connection if a later dispatch re-points the proxy in
210
+ // between, leaving this one paused forever -- the very stall the proxy exists
211
+ // to prevent.
106
212
  controller.pause()
107
- this.retryOpts.retry(
213
+ // The default policy returns its backoff timer so an abort can cancel it;
214
+ // a custom policy may return anything (or nothing), which is ignored.
215
+ this.retryPending = true
216
+ this.retryTimer = this.retryOpts.retry(
108
217
  err,
109
218
  {
110
219
  state: { counter: this.retryCount },
111
220
  opts: { retryOptions: this.retryOpts, ...this.opts }
112
221
  },
113
222
  shouldRetry.bind(this)
114
- )
223
+ ) ?? null
224
+ }
225
+
226
+ checkpointResponseEnd (headers) {
227
+ if (this.end == null && this.opts.method !== 'HEAD') {
228
+ const contentLength = headers['content-length']
229
+ this.end = contentLength != null ? Number(contentLength) - 1 : null
230
+
231
+ assert(
232
+ this.end == null || Number.isFinite(this.end),
233
+ 'invalid content-length'
234
+ )
235
+
236
+ this.resume = this.end != null
237
+ }
115
238
  }
116
239
 
117
240
  onRequestStart (controller, context) {
241
+ // request.js creates a fresh RequestController per dispatch and passes that
242
+ // same instance to every later callback of the dispatch. onRequestStart is
243
+ // the first callback (it is where the controller is created), so re-pointing
244
+ // the proxy here is enough to keep it on the active connection across every
245
+ // transparent retry/resume.
246
+ this.controllerProxy.target = controller
118
247
  if (!this.headersSent) {
119
- this.handler.onRequestStart?.(controller, context)
248
+ this.handler.onRequestStart?.(this.controllerProxy, context)
120
249
  }
121
250
  }
122
251
 
123
- onRequestUpgrade (controller, statusCode, headers, socket) {
124
- this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
252
+ onBodySent (chunk) {
253
+ this.handler.onBodySent?.(chunk)
254
+ }
255
+
256
+ onRequestSent () {
257
+ this.handler.onRequestSent?.()
258
+ }
259
+
260
+ onRequestUpgrade (_controller, statusCode, headers, socket) {
261
+ this.handler.onRequestUpgrade?.(this.controllerProxy, statusCode, headers, socket)
125
262
  }
126
263
 
127
264
  static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) {
@@ -134,7 +271,8 @@ class RetryHandler {
134
271
  timeoutFactor,
135
272
  statusCodes,
136
273
  errorCodes,
137
- methods
274
+ methods,
275
+ retryAfter
138
276
  } = retryOptions
139
277
  const { counter } = state
140
278
 
@@ -166,7 +304,7 @@ class RetryHandler {
166
304
  return
167
305
  }
168
306
 
169
- let retryAfterHeader = headers?.['retry-after']
307
+ let retryAfterHeader = retryAfter === false ? undefined : headers?.['retry-after']
170
308
  if (retryAfterHeader) {
171
309
  retryAfterHeader = Number(retryAfterHeader)
172
310
  retryAfterHeader = Number.isNaN(retryAfterHeader)
@@ -175,31 +313,28 @@ class RetryHandler {
175
313
  }
176
314
 
177
315
  const retryTimeout =
178
- retryAfterHeader > 0
179
- ? Math.min(retryAfterHeader, maxTimeout)
180
- : Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout)
181
-
182
- setTimeout(() => cb(null), retryTimeout)
316
+ retryAfterHeader === 0
317
+ ? 0
318
+ : retryAfterHeader > 0
319
+ ? Math.min(retryAfterHeader, maxTimeout)
320
+ : Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout)
321
+
322
+ // Return the backoff timer so the handler can cancel it when the
323
+ // consumer aborts while the retry decision is pending.
324
+ return setTimeout(() => cb(null), retryTimeout)
183
325
  }
184
326
 
185
327
  onResponseStart (controller, statusCode, headers, statusMessage) {
328
+ if (statusCode < 200) {
329
+ this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
330
+ return
331
+ }
332
+
186
333
  this.error = null
187
334
  this.retryCount += 1
188
335
  this.statusCode = statusCode
189
336
  this.headers = headers
190
337
 
191
- if (statusCode >= 300) {
192
- const err = new RequestRetryError('Request failed', statusCode, {
193
- headers,
194
- data: {
195
- count: this.retryCount
196
- }
197
- })
198
-
199
- this.onResponseStartWithRetry(controller, statusCode, headers, statusMessage, err)
200
- return
201
- }
202
-
203
338
  // Checkpoint for resume from where we left it
204
339
  if (this.headersSent) {
205
340
  // Only Partial Content 206 supposed to provide Content-Range,
@@ -232,23 +367,41 @@ class RetryHandler {
232
367
  })
233
368
  }
234
369
 
370
+ validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount)
371
+
235
372
  const { start, size, end = size ? size - 1 : null } = contentRange
236
373
 
237
- assert(this.start === start, 'content-range mismatch')
238
- assert(this.end == null || this.end === end, 'content-range mismatch')
374
+ if (this.start !== start || (this.end != null && this.end !== end)) {
375
+ throw new RequestRetryError('Content-Range mismatch', statusCode, {
376
+ headers,
377
+ data: { count: this.retryCount }
378
+ })
379
+ }
239
380
 
240
381
  return
241
382
  }
242
383
 
384
+ if (statusCode >= 300) {
385
+ const err = new RequestRetryError('Request failed', statusCode, {
386
+ headers,
387
+ data: {
388
+ count: this.retryCount
389
+ }
390
+ })
391
+
392
+ this.onResponseStartWithRetry(controller, statusCode, headers, statusMessage, err)
393
+ return
394
+ }
395
+
243
396
  if (this.end == null) {
244
397
  if (statusCode === 206) {
245
398
  // First time we receive 206
246
399
  const range = parseRangeHeader(headers['content-range'])
247
400
 
248
- if (range == null) {
401
+ if (range == null || range.end == null) {
249
402
  this.headersSent = true
250
403
  this.handler.onResponseStart?.(
251
- controller,
404
+ this.controllerProxy,
252
405
  statusCode,
253
406
  headers,
254
407
  statusMessage
@@ -256,6 +409,8 @@ class RetryHandler {
256
409
  return
257
410
  }
258
411
 
412
+ validatePartialResponseContentLength(headers, range, statusCode, this.retryCount)
413
+
259
414
  const { start, size, end = size ? size - 1 : null } = range
260
415
  assert(
261
416
  start != null && Number.isFinite(start),
@@ -268,7 +423,7 @@ class RetryHandler {
268
423
  }
269
424
 
270
425
  // We make our best to checkpoint the body for further range headers
271
- if (this.end == null) {
426
+ if (this.end == null && this.opts.method !== 'HEAD') {
272
427
  const contentLength = headers['content-length']
273
428
  this.end = contentLength != null ? Number(contentLength) - 1 : null
274
429
  }
@@ -295,7 +450,7 @@ class RetryHandler {
295
450
 
296
451
  this.headersSent = true
297
452
  this.handler.onResponseStart?.(
298
- controller,
453
+ this.controllerProxy,
299
454
  statusCode,
300
455
  headers,
301
456
  statusMessage
@@ -308,17 +463,17 @@ class RetryHandler {
308
463
  }
309
464
  }
310
465
 
311
- onResponseData (controller, chunk) {
466
+ onResponseData (_controller, chunk) {
312
467
  if (this.error) {
313
468
  return
314
469
  }
315
470
 
316
471
  this.start += chunk.length
317
472
 
318
- this.handler.onResponseData?.(controller, chunk)
473
+ this.handler.onResponseData?.(this.controllerProxy, chunk)
319
474
  }
320
475
 
321
- onResponseEnd (controller, trailers) {
476
+ onResponseEnd (_controller, trailers) {
322
477
  if (this.error && this.retryOpts.throwOnError) {
323
478
  throw this.error
324
479
  }
@@ -335,13 +490,13 @@ class RetryHandler {
335
490
  }
336
491
  }
337
492
  this.retryCount = 0
338
- return this.handler.onResponseEnd?.(controller, trailers)
493
+ return this.handler.onResponseEnd?.(this.controllerProxy, trailers)
339
494
  }
340
495
 
341
- this.retry(controller)
496
+ this.retry()
342
497
  }
343
498
 
344
- retry (controller) {
499
+ retry () {
345
500
  if (this.start !== 0) {
346
501
  const headers = { range: `bytes=${this.start}-${this.end ?? ''}` }
347
502
 
@@ -363,23 +518,38 @@ class RetryHandler {
363
518
  this.retryCountCheckpoint = this.retryCount
364
519
  this.dispatch(this.opts, this)
365
520
  } catch (err) {
366
- this.handler.onResponseError?.(controller, err)
521
+ this.handler.onResponseError?.(this.controllerProxy, err)
367
522
  }
368
523
  }
369
524
 
370
525
  onResponseError (controller, err) {
371
- if (controller?.aborted || isDisturbed(this.opts.body)) {
372
- this.handler.onResponseError?.(controller, err)
526
+ if (this.aborted) {
527
+ // #onAbort already delivered the terminal error downstream; the late
528
+ // error of the torn-down connection must not be forwarded twice.
529
+ return
530
+ }
531
+
532
+ // controller is THIS failed connection (not the proxy): we inspect whether
533
+ // the consumer aborted it to decide retry-vs-propagate.
534
+ if (controller?.aborted || isDisturbed(this.opts.body) || (this.headersSent && !this.resume)) {
535
+ this.handler.onResponseError?.(this.controllerProxy, err)
373
536
  return
374
537
  }
375
538
 
376
539
  function shouldRetry (returnedErr) {
540
+ if (this.aborted) {
541
+ // Aborted while the policy was deciding; the decision is moot.
542
+ return
543
+ }
544
+ this.retryPending = false
545
+ this.retryTimer = null
546
+
377
547
  if (!returnedErr) {
378
- this.retry(controller)
548
+ this.retry()
379
549
  return
380
550
  }
381
551
 
382
- this.handler?.onResponseError?.(controller, returnedErr)
552
+ this.handler?.onResponseError?.(this.controllerProxy, returnedErr)
383
553
  }
384
554
 
385
555
  // We reconcile in case of a mix between network errors
@@ -393,14 +563,31 @@ class RetryHandler {
393
563
  this.retryCount += 1
394
564
  }
395
565
 
396
- this.retryOpts.retry(
566
+ this.retryPending = true
567
+ this.retryTimer = this.retryOpts.retry(
397
568
  err,
398
569
  {
399
570
  state: { counter: this.retryCount },
400
571
  opts: { retryOptions: this.retryOpts, ...this.opts }
401
572
  },
402
573
  shouldRetry.bind(this)
403
- )
574
+ ) ?? null
575
+ }
576
+
577
+ #onAbort (reason) {
578
+ // A consumer abort lands on the controller proxy. If the retry policy is
579
+ // still deciding (typically holding a backoff timer), cancel the wait and
580
+ // surface the abort immediately instead of letting the request hang until
581
+ // the backoff elapses.
582
+ if (!this.retryPending) {
583
+ return
584
+ }
585
+
586
+ this.aborted = true
587
+ this.retryPending = false
588
+ clearTimeout(this.retryTimer)
589
+ this.retryTimer = null
590
+ this.handler.onResponseError?.(this.controllerProxy, reason ?? new RequestAbortedError())
404
591
  }
405
592
  }
406
593