@openclaw/discord 2026.6.11 → 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 (152) hide show
  1. package/dist/action-runtime-api.js +1 -1
  2. package/dist/{api-4GFMoWu0.js → api-CjnGBhM3.js} +4 -1
  3. package/dist/api.js +14 -14
  4. package/dist/{approval-handler.runtime-SsFtZQA6.js → approval-handler.runtime-BWPQrzpP.js} +9 -4
  5. package/dist/{audit-CAnmyf2A.js → audit-BwJ_2OjL.js} +3 -3
  6. package/dist/{channel-YA7AbYP3.js → channel-DU4jnQgl.js} +15 -15
  7. package/dist/{channel-actions-BJ3n5Z1l.js → channel-actions-CwmIqnYN.js} +2 -2
  8. package/dist/{channel-actions.runtime-D2QTRgZ9.js → channel-actions.runtime-DvpTfaJ4.js} +5 -5
  9. package/dist/channel-plugin-api.js +1 -1
  10. package/dist/{channel.setup-B1zDEAR7.js → channel.setup-glZ5Y98a.js} +2 -2
  11. package/dist/{components-Dm4c_ce6.js → components-j6lTZBsT.js} +1 -1
  12. package/dist/{conversation-identity-DZ0BD7xO.js → conversation-identity-36FFBwm5.js} +2 -2
  13. package/dist/{directory-config-BK1OhHa3.js → directory-config-BYMFzL29.js} +1 -1
  14. package/dist/directory-contract-api.js +1 -1
  15. package/dist/{directory-live-DP3CmwGf.js → directory-live-9JtJCiu8.js} +2 -2
  16. package/dist/{handle-action.guild-admin-B2cOoU2D.js → handle-action.guild-admin-xxGYrp-I.js} +2 -2
  17. package/dist/index.js +1 -1
  18. package/dist/{manager.runtime-CPHxr9B3.js → manager.runtime-NxCd-PVt.js} +3 -3
  19. package/dist/{message-handler-CKaTgBIV.js → message-handler-cizxbaDc.js} +7 -6
  20. package/dist/{message-handler.preflight--eAiaewz.js → message-handler.preflight-CbD0u7U7.js} +10 -10
  21. package/dist/{message-handler.process-PhKAmrEd.js → message-handler.process-Do4EM591.js} +10 -10
  22. package/dist/{message-utils-O1vK3kPu.js → message-utils-CJeAoISH.js} +1 -1
  23. package/dist/{outbound-adapter-fO_7hbTh.js → outbound-adapter-Dxs6iKEq.js} +62 -131
  24. package/dist/{pluralkit-B3MzPaPU.js → pluralkit-DI6RYaVj.js} +1 -1
  25. package/dist/presence-cache-z1N8MbJ3.js +45 -0
  26. package/dist/{probe-CxnDikWm.js → probe-esE_Oqdm.js} +1 -1
  27. package/dist/{probe.runtime-DLFkSDVB.js → probe.runtime-C3Atgpvr.js} +1 -1
  28. package/dist/{provider-BJaJ20MY.js → provider-B-s93q9s.js} +76 -26
  29. package/dist/{provider-session.runtime-D6xc-60s.js → provider-session.runtime-BKMOqHpd.js} +3 -3
  30. package/dist/provider.runtime-CaTJrpnl.js +2 -0
  31. package/dist/{resolve-allowlist-common-BDpBb8dF.js → resolve-allowlist-common-3gB2Gpmm.js} +1 -1
  32. package/dist/{resolve-channels-iJIjYfRw.js → resolve-channels-B3gQf8QL.js} +3 -3
  33. package/dist/{resolve-users-_fv1mm1f.js → resolve-users-BO10y5fJ.js} +3 -3
  34. package/dist/{runtime-ywXByivr.js → runtime-CFlR3L9X.js} +8 -8
  35. package/dist/runtime-api.actions.js +2 -2
  36. package/dist/runtime-api.js +20 -20
  37. package/dist/runtime-api.lookup.js +5 -5
  38. package/dist/runtime-api.monitor-CRiBIQEI.js +6 -0
  39. package/dist/runtime-api.monitor.js +6 -5
  40. package/dist/runtime-api.send.js +5 -5
  41. package/dist/runtime-api.threads.js +3 -3
  42. package/dist/{send-Dc5uQX5H.js → send-JHxfT4Pk.js} +6 -4
  43. package/dist/{send.components-D4bl20xr.js → send.components-CN35E2Ck.js} +11 -6
  44. package/dist/{send.outbound-5axvwLkl.js → send.outbound-B-cA1Esv.js} +15 -8
  45. package/dist/{send.receipt-B21MspvT.js → send.receipt-CGecJDin.js} +10 -1
  46. package/dist/{send.shared-9082A2sO.js → send.shared-vcbfThdy.js} +107 -16
  47. package/dist/session-binding-contract-api.js +1 -1
  48. package/dist/setup-plugin-api.js +1 -1
  49. package/dist/{subagent-hooks-NTv6B4up.js → subagent-hooks-20wXhVzZ.js} +2 -2
  50. package/dist/subagent-hooks-api.js +1 -1
  51. package/dist/{system-events-BzaI1WpN.js → system-events-DR27TE7p.js} +1 -1
  52. package/dist/{target-resolver-agnOrtOU.js → target-resolver-CDi1gI90.js} +2 -2
  53. package/dist/targets-C6yd3y8R.js +3 -0
  54. package/dist/{thread-bindings-DNitaHVh.js → thread-bindings-DU4T6nDo.js} +4 -4
  55. package/dist/{thread-bindings.discord-api-CjYNuvZa.js → thread-bindings.discord-api-CYkr-3Gj.js} +4 -4
  56. package/dist/{thread-bindings.manager-PZvJ4TJx.js → thread-bindings.manager-BqWs1UE2.js} +3 -3
  57. package/dist/{transcripts-source-Bi8N6h5X.js → transcripts-source-D-lBI5ZD.js} +1 -1
  58. package/dist/transcripts-source-api.js +1 -1
  59. package/dist/{typing-BdRqXUN3.js → typing-xr05bL-r.js} +1 -1
  60. package/node_modules/undici/README.md +8 -2
  61. package/node_modules/undici/docs/docs/api/Agent.md +248 -44
  62. package/node_modules/undici/docs/docs/api/BalancedPool.md +246 -48
  63. package/node_modules/undici/docs/docs/api/CacheStorage.md +171 -13
  64. package/node_modules/undici/docs/docs/api/CacheStore.md +294 -98
  65. package/node_modules/undici/docs/docs/api/Client.md +369 -179
  66. package/node_modules/undici/docs/docs/api/ClientStats.md +80 -14
  67. package/node_modules/undici/docs/docs/api/Connector.md +118 -36
  68. package/node_modules/undici/docs/docs/api/ContentType.md +65 -25
  69. package/node_modules/undici/docs/docs/api/Cookies.md +122 -71
  70. package/node_modules/undici/docs/docs/api/Debug.md +34 -18
  71. package/node_modules/undici/docs/docs/api/DiagnosticsChannel.md +364 -164
  72. package/node_modules/undici/docs/docs/api/Dispatcher.md +485 -1102
  73. package/node_modules/undici/docs/docs/api/EnvHttpProxyAgent.md +101 -87
  74. package/node_modules/undici/docs/docs/api/Errors.md +503 -40
  75. package/node_modules/undici/docs/docs/api/EventSource.md +294 -32
  76. package/node_modules/undici/docs/docs/api/Fetch.md +680 -31
  77. package/node_modules/undici/docs/docs/api/GlobalInstallation.md +78 -98
  78. package/node_modules/undici/docs/docs/api/H2CClient.md +298 -176
  79. package/node_modules/undici/docs/docs/api/Interceptors.md +414 -0
  80. package/node_modules/undici/docs/docs/api/MockAgent.md +333 -314
  81. package/node_modules/undici/docs/docs/api/MockCallHistory.md +272 -98
  82. package/node_modules/undici/docs/docs/api/MockCallHistoryLog.md +189 -26
  83. package/node_modules/undici/docs/docs/api/MockClient.md +151 -33
  84. package/node_modules/undici/docs/docs/api/MockErrors.md +56 -5
  85. package/node_modules/undici/docs/docs/api/MockPool.md +321 -258
  86. package/node_modules/undici/docs/docs/api/Pool.md +235 -43
  87. package/node_modules/undici/docs/docs/api/PoolStats.md +119 -13
  88. package/node_modules/undici/docs/docs/api/ProxyAgent.md +179 -160
  89. package/node_modules/undici/docs/docs/api/RedirectHandler.md +238 -63
  90. package/node_modules/undici/docs/docs/api/RetryAgent.md +113 -26
  91. package/node_modules/undici/docs/docs/api/RetryHandler.md +161 -77
  92. package/node_modules/undici/docs/docs/api/RoundRobinPool.md +166 -72
  93. package/node_modules/undici/docs/docs/api/SnapshotAgent.md +264 -542
  94. package/node_modules/undici/docs/docs/api/Socks5ProxyAgent.md +162 -187
  95. package/node_modules/undici/docs/docs/api/Util.md +68 -11
  96. package/node_modules/undici/docs/docs/api/WebSocket.md +475 -80
  97. package/node_modules/undici/docs/docs/api/api-lifecycle.md +100 -32
  98. package/node_modules/undici/docs/docs/best-practices/client-certificate.md +2 -0
  99. package/node_modules/undici/docs/docs/best-practices/crawling.md +3 -1
  100. package/node_modules/undici/docs/docs/best-practices/migrating-from-v7-to-v8.md +6 -4
  101. package/node_modules/undici/docs/docs/best-practices/mocking-request.md +9 -7
  102. package/node_modules/undici/docs/docs/best-practices/proxy.md +3 -1
  103. package/node_modules/undici/docs/docs/best-practices/undici-vs-builtin-fetch.md +10 -8
  104. package/node_modules/undici/docs/docs/best-practices/writing-tests.md +2 -0
  105. package/node_modules/undici/docs/docs/{GettingStarted.md → getting-started.md} +22 -19
  106. package/node_modules/undici/docs/docs/index.md +784 -0
  107. package/node_modules/undici/docs/docs/site.json +125 -0
  108. package/node_modules/undici/docs/docs/type-map.json +79 -0
  109. package/node_modules/undici/lib/api/api-request.js +7 -1
  110. package/node_modules/undici/lib/api/readable.js +47 -2
  111. package/node_modules/undici/lib/cache/memory-cache-store.js +54 -9
  112. package/node_modules/undici/lib/cache/sqlite-cache-store.js +7 -1
  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/util.js +30 -2
  116. package/node_modules/undici/lib/dispatcher/agent.js +3 -3
  117. package/node_modules/undici/lib/dispatcher/client-h1.js +65 -8
  118. package/node_modules/undici/lib/dispatcher/client-h2.js +265 -142
  119. package/node_modules/undici/lib/dispatcher/proxy-agent.js +40 -6
  120. package/node_modules/undici/lib/global.js +36 -16
  121. package/node_modules/undici/lib/handler/cache-handler.js +269 -61
  122. package/node_modules/undici/lib/handler/cache-revalidation-handler.js +13 -3
  123. package/node_modules/undici/lib/handler/redirect-handler.js +6 -2
  124. package/node_modules/undici/lib/handler/retry-handler.js +87 -23
  125. package/node_modules/undici/lib/interceptor/cache.js +159 -52
  126. package/node_modules/undici/lib/interceptor/decompress.js +9 -2
  127. package/node_modules/undici/lib/mock/mock-interceptor.js +25 -7
  128. package/node_modules/undici/lib/mock/mock-utils.js +39 -10
  129. package/node_modules/undici/lib/util/cache.js +372 -68
  130. package/node_modules/undici/lib/util/date.js +23 -6
  131. package/node_modules/undici/lib/web/cookies/parse.js +3 -2
  132. package/node_modules/undici/lib/web/cookies/util.js +79 -9
  133. package/node_modules/undici/lib/web/eventsource/util.js +1 -1
  134. package/node_modules/undici/lib/web/fetch/body.js +2 -1
  135. package/node_modules/undici/lib/web/fetch/constants.js +1 -1
  136. package/node_modules/undici/lib/web/fetch/index.js +11 -2
  137. package/node_modules/undici/lib/web/fetch/response.js +1 -3
  138. package/node_modules/undici/lib/web/fetch/util.js +4 -1
  139. package/node_modules/undici/package.json +2 -2
  140. package/node_modules/undici/types/client.d.ts +3 -3
  141. package/node_modules/undici/types/cookies.d.ts +1 -1
  142. package/node_modules/undici/types/dispatcher.d.ts +3 -3
  143. package/node_modules/undici/types/errors.d.ts +10 -0
  144. package/node_modules/undici/types/handlers.d.ts +2 -0
  145. package/node_modules/undici/types/mock-interceptor.d.ts +5 -1
  146. package/node_modules/undici/types/proxy-agent.d.ts +7 -0
  147. package/npm-shrinkwrap.json +7 -7
  148. package/package.json +5 -5
  149. package/dist/gateway-registry-DPxmW0Db.js +0 -74
  150. package/dist/provider.runtime-jNlHxUSW.js +0 -2
  151. package/dist/runtime-api.monitor-Bb5AaRzg.js +0 -6
  152. package/dist/targets-ClUPljCU.js +0 -3
@@ -11,7 +11,49 @@ 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
+ class RetryController {
45
+ constructor () {
46
+ this.target = null
47
+ }
48
+
49
+ pause () { this.target?.pause() }
50
+ resume () { this.target?.resume() }
51
+ abort (reason) { this.target?.abort(reason) }
52
+ get paused () { return this.target?.paused ?? false }
53
+ get aborted () { return this.target?.aborted ?? false }
54
+ get reason () { return this.target?.reason ?? null }
55
+ get rawHeaders () { return this.target?.rawHeaders ?? null }
56
+ get rawTrailers () { return this.target?.rawTrailers ?? null }
15
57
  }
16
58
 
17
59
  class RetryHandler {
@@ -45,7 +87,7 @@ class RetryHandler {
45
87
  timeoutFactor: timeoutFactor ?? 2,
46
88
  maxRetries: maxRetries ?? 5,
47
89
  // What errors we should retry
48
- methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'],
90
+ methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE', 'QUERY'],
49
91
  // Indicates which errors to retry
50
92
  statusCodes: statusCodes ?? [500, 502, 503, 504, 429],
51
93
  // List of errors to retry
@@ -70,6 +112,7 @@ class RetryHandler {
70
112
  this.etag = null
71
113
  this.statusCode = null
72
114
  this.headers = null
115
+ this.controllerProxy = new RetryController()
73
116
  }
74
117
 
75
118
  onResponseStartWithRetry (controller, statusCode, headers, statusMessage, err) {
@@ -77,7 +120,7 @@ class RetryHandler {
77
120
  // Preserve old behavior for status codes that are not eligible for retry
78
121
  if (this.retryOpts.statusCodes.includes(statusCode) === false) {
79
122
  this.headersSent = true
80
- this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
123
+ this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
81
124
  } else {
82
125
  this.error = err
83
126
  }
@@ -87,14 +130,14 @@ class RetryHandler {
87
130
 
88
131
  if (isDisturbed(this.opts.body)) {
89
132
  this.headersSent = true
90
- this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
133
+ this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
91
134
  return
92
135
  }
93
136
 
94
137
  function shouldRetry (passedErr) {
95
138
  if (passedErr) {
96
139
  this.headersSent = true
97
- this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
140
+ this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
98
141
  controller.resume()
99
142
  return
100
143
  }
@@ -103,6 +146,13 @@ class RetryHandler {
103
146
  controller.resume()
104
147
  }
105
148
 
149
+ // The pause()/resume() pair (here and in shouldRetry) acts on THIS
150
+ // connection's controller -- never the downstream proxy. We hold this exact
151
+ // connection while the retry policy decides (possibly after a timeout) and
152
+ // must resume the same one. Routing this through controllerProxy would risk
153
+ // resuming a different connection if a later dispatch re-points the proxy in
154
+ // between, leaving this one paused forever -- the very stall the proxy exists
155
+ // to prevent.
106
156
  controller.pause()
107
157
  this.retryOpts.retry(
108
158
  err,
@@ -115,13 +165,19 @@ class RetryHandler {
115
165
  }
116
166
 
117
167
  onRequestStart (controller, context) {
168
+ // request.js creates a fresh RequestController per dispatch and passes that
169
+ // same instance to every later callback of the dispatch. onRequestStart is
170
+ // the first callback (it is where the controller is created), so re-pointing
171
+ // the proxy here is enough to keep it on the active connection across every
172
+ // transparent retry/resume.
173
+ this.controllerProxy.target = controller
118
174
  if (!this.headersSent) {
119
- this.handler.onRequestStart?.(controller, context)
175
+ this.handler.onRequestStart?.(this.controllerProxy, context)
120
176
  }
121
177
  }
122
178
 
123
- onRequestUpgrade (controller, statusCode, headers, socket) {
124
- this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
179
+ onRequestUpgrade (_controller, statusCode, headers, socket) {
180
+ this.handler.onRequestUpgrade?.(this.controllerProxy, statusCode, headers, socket)
125
181
  }
126
182
 
127
183
  static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) {
@@ -175,9 +231,11 @@ class RetryHandler {
175
231
  }
176
232
 
177
233
  const retryTimeout =
178
- retryAfterHeader > 0
179
- ? Math.min(retryAfterHeader, maxTimeout)
180
- : Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout)
234
+ retryAfterHeader === 0
235
+ ? 0
236
+ : retryAfterHeader > 0
237
+ ? Math.min(retryAfterHeader, maxTimeout)
238
+ : Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout)
181
239
 
182
240
  setTimeout(() => cb(null), retryTimeout)
183
241
  }
@@ -232,6 +290,8 @@ class RetryHandler {
232
290
  })
233
291
  }
234
292
 
293
+ validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount)
294
+
235
295
  const { start, size, end = size ? size - 1 : null } = contentRange
236
296
 
237
297
  assert(this.start === start, 'content-range mismatch')
@@ -248,7 +308,7 @@ class RetryHandler {
248
308
  if (range == null) {
249
309
  this.headersSent = true
250
310
  this.handler.onResponseStart?.(
251
- controller,
311
+ this.controllerProxy,
252
312
  statusCode,
253
313
  headers,
254
314
  statusMessage
@@ -256,6 +316,8 @@ class RetryHandler {
256
316
  return
257
317
  }
258
318
 
319
+ validatePartialResponseContentLength(headers, range, statusCode, this.retryCount)
320
+
259
321
  const { start, size, end = size ? size - 1 : null } = range
260
322
  assert(
261
323
  start != null && Number.isFinite(start),
@@ -295,7 +357,7 @@ class RetryHandler {
295
357
 
296
358
  this.headersSent = true
297
359
  this.handler.onResponseStart?.(
298
- controller,
360
+ this.controllerProxy,
299
361
  statusCode,
300
362
  headers,
301
363
  statusMessage
@@ -308,17 +370,17 @@ class RetryHandler {
308
370
  }
309
371
  }
310
372
 
311
- onResponseData (controller, chunk) {
373
+ onResponseData (_controller, chunk) {
312
374
  if (this.error) {
313
375
  return
314
376
  }
315
377
 
316
378
  this.start += chunk.length
317
379
 
318
- this.handler.onResponseData?.(controller, chunk)
380
+ this.handler.onResponseData?.(this.controllerProxy, chunk)
319
381
  }
320
382
 
321
- onResponseEnd (controller, trailers) {
383
+ onResponseEnd (_controller, trailers) {
322
384
  if (this.error && this.retryOpts.throwOnError) {
323
385
  throw this.error
324
386
  }
@@ -335,13 +397,13 @@ class RetryHandler {
335
397
  }
336
398
  }
337
399
  this.retryCount = 0
338
- return this.handler.onResponseEnd?.(controller, trailers)
400
+ return this.handler.onResponseEnd?.(this.controllerProxy, trailers)
339
401
  }
340
402
 
341
- this.retry(controller)
403
+ this.retry()
342
404
  }
343
405
 
344
- retry (controller) {
406
+ retry () {
345
407
  if (this.start !== 0) {
346
408
  const headers = { range: `bytes=${this.start}-${this.end ?? ''}` }
347
409
 
@@ -363,23 +425,25 @@ class RetryHandler {
363
425
  this.retryCountCheckpoint = this.retryCount
364
426
  this.dispatch(this.opts, this)
365
427
  } catch (err) {
366
- this.handler.onResponseError?.(controller, err)
428
+ this.handler.onResponseError?.(this.controllerProxy, err)
367
429
  }
368
430
  }
369
431
 
370
432
  onResponseError (controller, err) {
433
+ // controller is THIS failed connection (not the proxy): we inspect whether
434
+ // the consumer aborted it to decide retry-vs-propagate.
371
435
  if (controller?.aborted || isDisturbed(this.opts.body)) {
372
- this.handler.onResponseError?.(controller, err)
436
+ this.handler.onResponseError?.(this.controllerProxy, err)
373
437
  return
374
438
  }
375
439
 
376
440
  function shouldRetry (returnedErr) {
377
441
  if (!returnedErr) {
378
- this.retry(controller)
442
+ this.retry()
379
443
  return
380
444
  }
381
445
 
382
- this.handler?.onResponseError?.(controller, returnedErr)
446
+ this.handler?.onResponseError?.(this.controllerProxy, returnedErr)
383
447
  }
384
448
 
385
449
  // We reconcile in case of a mix between network errors
@@ -6,8 +6,9 @@ const util = require('../core/util')
6
6
  const CacheHandler = require('../handler/cache-handler')
7
7
  const MemoryCacheStore = require('../cache/memory-cache-store')
8
8
  const CacheRevalidationHandler = require('../handler/cache-revalidation-handler')
9
- const { assertCacheStore, assertCacheMethods, makeCacheKey, normalizeHeaders, parseCacheControlHeader } = require('../util/cache.js')
9
+ const { assertCacheStore, assertCacheMethods, makeCacheKey, normalizeHeaders, parseCacheControlHeader, isInvalidOrWildcardVaryHeader } = require('../util/cache.js')
10
10
  const { AbortError } = require('../core/errors.js')
11
+ const { parseHttpDate } = require('../util/date.js')
11
12
 
12
13
  /**
13
14
  * @param {(string | RegExp)[] | undefined} origins
@@ -28,6 +29,44 @@ function assertCacheOrigins (origins, name) {
28
29
 
29
30
  const nop = () => {}
30
31
 
32
+ function trimOWS (value) {
33
+ return value.replace(/^[\t ]+|[\t ]+$/g, '')
34
+ }
35
+
36
+ function arrayIncludes (array, value) {
37
+ for (let i = 0; i < array.length; i++) {
38
+ if (array[i] === value) {
39
+ return true
40
+ }
41
+ }
42
+
43
+ return false
44
+ }
45
+
46
+ function hasPragmaNoCache (headers) {
47
+ const pragma = headers?.pragma
48
+ if (!pragma) {
49
+ return false
50
+ }
51
+
52
+ const values = Array.isArray(pragma) ? pragma : [pragma]
53
+ for (let i = 0; i < values.length; i++) {
54
+ const value = values[i]
55
+ if (typeof value !== 'string') {
56
+ continue
57
+ }
58
+
59
+ const directives = value.split(',')
60
+ for (let j = 0; j < directives.length; j++) {
61
+ if (trimOWS(directives[j]).toLowerCase() === 'no-cache') {
62
+ return true
63
+ }
64
+ }
65
+ }
66
+
67
+ return false
68
+ }
69
+
31
70
  /**
32
71
  * @typedef {(options: import('../../types/dispatcher.d.ts').default.DispatchOptions, handler: import('../../types/dispatcher.d.ts').default.DispatchHandler) => void} DispatchFn
33
72
  */
@@ -57,16 +96,92 @@ function needsRevalidation (result, cacheControlDirectives, { headers = {} }) {
57
96
  return false
58
97
  }
59
98
 
99
+ /**
100
+ * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
101
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
102
+ * @returns {boolean}
103
+ */
104
+ function staleResponseRequiresRevalidation (result, cacheType) {
105
+ return result.cacheControlDirectives?.['must-revalidate'] === true ||
106
+ (cacheType === 'shared' && (
107
+ result.cacheControlDirectives?.['proxy-revalidate'] === true ||
108
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10
109
+ // s-maxage implies proxy-revalidate for shared caches.
110
+ result.cacheControlDirectives?.['s-maxage'] !== undefined
111
+ ))
112
+ }
113
+
114
+ /**
115
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
116
+ * @param {import('../../types/header.d.ts').IncomingHttpHeaders} headers
117
+ * @returns {boolean}
118
+ */
119
+ function revalidationResponseDisallowsCachedReuse (cacheType, headers) {
120
+ if (headers.vary && isInvalidOrWildcardVaryHeader(headers.vary)) {
121
+ return true
122
+ }
123
+
124
+ const cacheControl = headers['cache-control']
125
+ if (!cacheControl) {
126
+ return false
127
+ }
128
+
129
+ const cacheControlDirectives = parseCacheControlHeader(cacheControl)
130
+ return cacheControlDirectives['no-store'] === true ||
131
+ (cacheType === 'shared' && cacheControlDirectives.private === true)
132
+ }
133
+
134
+ function revalidationResponseUpdatesCacheControl (headers) {
135
+ return headers['cache-control'] !== undefined
136
+ }
137
+
138
+ function deleteCachedValue (store, cacheKey) {
139
+ try {
140
+ store.delete(cacheKey)?.catch?.(nop)
141
+ } catch {
142
+ // Fail silently
143
+ }
144
+ }
145
+
146
+ function getUsableLastModified (headers) {
147
+ const lastModified = headers?.['last-modified']
148
+ if (typeof lastModified === 'string' && parseHttpDate(lastModified)) {
149
+ return lastModified
150
+ }
151
+ }
152
+
153
+ function makeRevalidationHeaders (opts, result) {
154
+ const headers = {
155
+ ...opts.headers,
156
+ 'if-modified-since': getUsableLastModified(result.headers) ?? new Date(result.cachedAt).toUTCString()
157
+ }
158
+
159
+ if (result.etag) {
160
+ headers['if-none-match'] = result.etag
161
+ }
162
+
163
+ if (result.vary) {
164
+ for (const key in result.vary) {
165
+ if (result.vary[key] != null) {
166
+ headers[key] = result.vary[key]
167
+ }
168
+ }
169
+ }
170
+
171
+ return headers
172
+ }
173
+
60
174
  /**
61
175
  * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
62
176
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives
177
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
63
178
  * @returns {boolean}
64
179
  */
65
- function isStale (result, cacheControlDirectives) {
180
+ function isStale (result, cacheControlDirectives, cacheType) {
66
181
  const now = Date.now()
67
182
  if (now > result.staleAt) {
68
183
  // Response is stale
69
- if (cacheControlDirectives?.['max-stale']) {
184
+ if (!staleResponseRequiresRevalidation(result, cacheType) && cacheControlDirectives?.['max-stale']) {
70
185
  // There's a threshold where we can serve stale responses, let's see if
71
186
  // we're in it
72
187
  // https://www.rfc-editor.org/rfc/rfc9111.html#name-max-stale
@@ -93,11 +208,12 @@ function isStale (result, cacheControlDirectives) {
93
208
  /**
94
209
  * Check if we're within the stale-while-revalidate window for a stale response
95
210
  * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
211
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
96
212
  * @returns {boolean}
97
213
  */
98
- function withinStaleWhileRevalidateWindow (result) {
214
+ function withinStaleWhileRevalidateWindow (result, cacheType) {
99
215
  const staleWhileRevalidate = result.cacheControlDirectives?.['stale-while-revalidate']
100
- if (!staleWhileRevalidate) {
216
+ if (!staleWhileRevalidate || staleResponseRequiresRevalidation(result, cacheType)) {
101
217
  return false
102
218
  }
103
219
 
@@ -280,14 +396,10 @@ function handleResult (
280
396
  }
281
397
 
282
398
  const age = Math.round((now - result.cachedAt) / 1000)
283
- if (reqCacheControl?.['max-age'] && age >= reqCacheControl['max-age']) {
284
- // Response is considered expired for this specific request
285
- // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1
286
- return dispatch(opts, handler)
287
- }
399
+ const requestMaxAgeExpired = reqCacheControl?.['max-age'] !== undefined && age >= reqCacheControl['max-age']
288
400
 
289
- const stale = isStale(result, reqCacheControl)
290
- const revalidate = needsRevalidation(result, reqCacheControl, opts)
401
+ const stale = requestMaxAgeExpired || isStale(result, reqCacheControl, globalOpts.type)
402
+ const revalidate = requestMaxAgeExpired || needsRevalidation(result, reqCacheControl, opts)
291
403
 
292
404
  // Check if the response is stale
293
405
  if (stale || revalidate) {
@@ -299,28 +411,13 @@ function handleResult (
299
411
 
300
412
  // RFC 5861: If we're within stale-while-revalidate window, serve stale immediately
301
413
  // and revalidate in background, unless immediate revalidation is necessary
302
- if (!revalidate && withinStaleWhileRevalidateWindow(result)) {
414
+ if (!revalidate && withinStaleWhileRevalidateWindow(result, globalOpts.type)) {
303
415
  // Serve stale response immediately
304
416
  sendCachedValue(handler, opts, result, age, null, true)
305
417
 
306
418
  // Start background revalidation (fire-and-forget)
307
419
  queueMicrotask(() => {
308
- const headers = {
309
- ...opts.headers,
310
- 'if-modified-since': new Date(result.cachedAt).toUTCString()
311
- }
312
-
313
- if (result.etag) {
314
- headers['if-none-match'] = result.etag
315
- }
316
-
317
- if (result.vary) {
318
- for (const key in result.vary) {
319
- if (result.vary[key] != null) {
320
- headers[key] = result.vary[key]
321
- }
322
- }
323
- }
420
+ const headers = makeRevalidationHeaders(opts, result)
324
421
 
325
422
  // Background revalidation - update cache if we get new data
326
423
  dispatch(
@@ -344,28 +441,15 @@ function handleResult (
344
441
  }
345
442
 
346
443
  let withinStaleIfErrorThreshold = false
347
- const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error']
348
- if (staleIfErrorExpiry) {
349
- withinStaleIfErrorThreshold = now < (result.staleAt + (staleIfErrorExpiry * 1000))
350
- }
351
-
352
- const headers = {
353
- ...opts.headers,
354
- 'if-modified-since': new Date(result.cachedAt).toUTCString()
355
- }
356
-
357
- if (result.etag) {
358
- headers['if-none-match'] = result.etag
359
- }
360
-
361
- if (result.vary) {
362
- for (const key in result.vary) {
363
- if (result.vary[key] != null) {
364
- headers[key] = result.vary[key]
365
- }
444
+ if (!staleResponseRequiresRevalidation(result, globalOpts.type)) {
445
+ const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error']
446
+ if (staleIfErrorExpiry) {
447
+ withinStaleIfErrorThreshold = now < (result.staleAt + (staleIfErrorExpiry * 1000))
366
448
  }
367
449
  }
368
450
 
451
+ const headers = makeRevalidationHeaders(opts, result)
452
+
369
453
  // We need to revalidate the response
370
454
  return dispatch(
371
455
  {
@@ -373,8 +457,23 @@ function handleResult (
373
457
  headers
374
458
  },
375
459
  new CacheRevalidationHandler(
376
- (success, context) => {
460
+ (success, context, statusCode, headers) => {
377
461
  if (success) {
462
+ if (statusCode === 304) {
463
+ if (revalidationResponseDisallowsCachedReuse(globalOpts.type, headers)) {
464
+ if (util.isStream(result.body)) {
465
+ result.body.on('error', nop).destroy()
466
+ }
467
+
468
+ deleteCachedValue(globalOpts.store, cacheKey)
469
+ return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
470
+ }
471
+
472
+ if (revalidationResponseUpdatesCacheControl(headers)) {
473
+ deleteCachedValue(globalOpts.store, cacheKey)
474
+ }
475
+ }
476
+
378
477
  // TODO: successful revalidation should be considered fresh (not give stale warning).
379
478
  sendCachedValue(handler, opts, result, age, context, stale)
380
479
  } else if (util.isStream(result.body)) {
@@ -431,11 +530,17 @@ module.exports = (opts = {}) => {
431
530
  type
432
531
  }
433
532
 
434
- const safeMethodsToNotCache = util.safeHTTPMethods.filter(method => methods.includes(method) === false)
533
+ const safeMethodsToNotCache = []
534
+ for (let i = 0; i < util.safeHTTPMethods.length; i++) {
535
+ const method = util.safeHTTPMethods[i]
536
+ if (!arrayIncludes(methods, method)) {
537
+ safeMethodsToNotCache.push(method)
538
+ }
539
+ }
435
540
 
436
541
  return dispatch => {
437
542
  return (opts, handler) => {
438
- if (!opts.origin || safeMethodsToNotCache.includes(opts.method)) {
543
+ if (!opts.origin || arrayIncludes(safeMethodsToNotCache, opts.method)) {
439
544
  // Not a method we want to cache or we don't have the origin, skip
440
545
  return dispatch(opts, handler)
441
546
  }
@@ -470,7 +575,9 @@ module.exports = (opts = {}) => {
470
575
 
471
576
  const reqCacheControl = opts.headers?.['cache-control']
472
577
  ? parseCacheControlHeader(opts.headers['cache-control'])
473
- : undefined
578
+ : hasPragmaNoCache(opts.headers)
579
+ ? { 'no-cache': true }
580
+ : undefined
474
581
 
475
582
  if (reqCacheControl?.['no-store']) {
476
583
  return dispatch(opts, handler)
@@ -32,6 +32,8 @@ let warningEmitted = /** @type {boolean} */ (false)
32
32
  class DecompressHandler extends DecoratorHandler {
33
33
  /** @type {Transform[]} */
34
34
  #decompressors = []
35
+ /** @type {Record<string, string | string[]> | undefined} */
36
+ #trailers
35
37
  /** @type {Readonly<number[]>} */
36
38
  #skipStatusCodes
37
39
  /** @type {boolean} */
@@ -123,7 +125,7 @@ class DecompressHandler extends DecoratorHandler {
123
125
  this.#setupDecompressorEvents(decompressor, controller)
124
126
 
125
127
  decompressor.on('end', () => {
126
- super.onResponseEnd(controller, {})
128
+ super.onResponseEnd(controller, this.#trailers)
127
129
  })
128
130
  }
129
131
 
@@ -141,7 +143,7 @@ class DecompressHandler extends DecoratorHandler {
141
143
  super.onResponseError(controller, err)
142
144
  return
143
145
  }
144
- super.onResponseEnd(controller, {})
146
+ super.onResponseEnd(controller, this.#trailers)
145
147
  })
146
148
  }
147
149
 
@@ -236,6 +238,7 @@ class DecompressHandler extends DecoratorHandler {
236
238
  */
237
239
  onResponseEnd (controller, trailers) {
238
240
  if (this.#decompressors.length > 0) {
241
+ this.#trailers = trailers
239
242
  this.#decompressors[0].end()
240
243
  this.#cleanupDecompressors()
241
244
  return
@@ -276,6 +279,10 @@ function createDecompressInterceptor (options = {}) {
276
279
 
277
280
  return (dispatch) => {
278
281
  return (opts, handler) => {
282
+ if (opts.method === 'HEAD') {
283
+ return dispatch(opts, handler)
284
+ }
285
+
279
286
  const decompressHandler = new DecompressHandler(handler, options)
280
287
  return dispatch(opts, decompressHandler)
281
288
  }
@@ -12,6 +12,11 @@ const {
12
12
  } = require('./mock-symbols')
13
13
  const { InvalidArgumentError } = require('../core/errors')
14
14
  const { serializePathWithQuery } = require('../core/util')
15
+ const {
16
+ types: {
17
+ isPromise
18
+ }
19
+ } = require('node:util')
15
20
 
16
21
  /**
17
22
  * Defines the scope API for an interceptor reply
@@ -117,13 +122,9 @@ class MockInterceptor {
117
122
  // Values of reply aren't available right now as they
118
123
  // can only be available when the reply callback is invoked.
119
124
  if (typeof replyOptionsCallbackOrStatusCode === 'function') {
120
- // We'll first wrap the provided callback in another function,
121
- // this function will properly resolve the data from the callback
122
- // when invoked.
123
- const wrappedDefaultsCallback = (opts) => {
124
- // Our reply options callback contains the parameter for statusCode, data and options.
125
- const resolvedData = replyOptionsCallbackOrStatusCode(opts)
126
-
125
+ // Resolves the data returned by a reply options callback into
126
+ // dispatch data, validating its format along the way.
127
+ const resolveReplyCallbackData = (resolvedData) => {
127
128
  // Check if it is in the right format
128
129
  if (typeof resolvedData !== 'object' || resolvedData === null) {
129
130
  throw new InvalidArgumentError('reply options callback must return an object')
@@ -138,6 +139,23 @@ class MockInterceptor {
138
139
  }
139
140
  }
140
141
 
142
+ // We'll first wrap the provided callback in another function,
143
+ // this function will properly resolve the data from the callback
144
+ // when invoked.
145
+ const wrappedDefaultsCallback = (opts) => {
146
+ // Our reply options callback contains the parameter for statusCode, data and options.
147
+ const resolvedData = replyOptionsCallbackOrStatusCode(opts)
148
+
149
+ // An asynchronous reply options callback resolves to the reply
150
+ // parameters, so the dispatch data can only be resolved once the
151
+ // returned promise settles.
152
+ if (isPromise(resolvedData)) {
153
+ return resolvedData.then(resolveReplyCallbackData)
154
+ }
155
+
156
+ return resolveReplyCallbackData(resolvedData)
157
+ }
158
+
141
159
  // Add usual dispatch data, but this time set the data parameter to function that will eventually provide data.
142
160
  const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], wrappedDefaultsCallback, { ignoreTrailingSlash: this[kIgnoreTrailingSlash] })
143
161
  return new MockScope(newMockDispatch)