@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c

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 (392) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +126 -16
  3. package/dist/bin/rango.js +319 -95
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +2724 -1053
  6. package/package.json +68 -14
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +64 -2
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +224 -32
  11. package/skills/caching/SKILL.md +279 -17
  12. package/skills/composability/SKILL.md +27 -3
  13. package/skills/css/SKILL.md +76 -0
  14. package/skills/debug-manifest/SKILL.md +4 -2
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +11 -9
  17. package/skills/hooks/SKILL.md +243 -29
  18. package/skills/host-router/SKILL.md +83 -23
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +68 -19
  21. package/skills/layout/SKILL.md +13 -9
  22. package/skills/links/SKILL.md +190 -23
  23. package/skills/loader/SKILL.md +235 -9
  24. package/skills/middleware/SKILL.md +18 -10
  25. package/skills/migrate-nextjs/SKILL.md +43 -19
  26. package/skills/migrate-react-router/SKILL.md +8 -2
  27. package/skills/mime-routes/SKILL.md +28 -1
  28. package/skills/observability/SKILL.md +172 -0
  29. package/skills/parallel/SKILL.md +18 -7
  30. package/skills/prerender/SKILL.md +65 -60
  31. package/skills/rango/SKILL.md +251 -24
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +115 -48
  34. package/skills/route/SKILL.md +46 -5
  35. package/skills/router-setup/SKILL.md +30 -8
  36. package/skills/scripts/SKILL.md +179 -0
  37. package/skills/server-actions/SKILL.md +775 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +130 -0
  40. package/skills/testing/bindings.md +103 -0
  41. package/skills/testing/cache-prerender.md +127 -0
  42. package/skills/testing/client-components.md +124 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +91 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +122 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +123 -0
  54. package/skills/typesafety/SKILL.md +322 -29
  55. package/skills/use-cache/SKILL.md +57 -14
  56. package/skills/view-transitions/SKILL.md +337 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +0 -65
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/connection-warmup.ts +134 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +192 -150
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/navigation-bridge.ts +94 -25
  70. package/src/browser/navigation-client.ts +121 -84
  71. package/src/browser/navigation-store-handle.ts +38 -0
  72. package/src/browser/navigation-store.ts +115 -67
  73. package/src/browser/navigation-transaction.ts +9 -59
  74. package/src/browser/network-error-handler.ts +34 -7
  75. package/src/browser/partial-update.ts +147 -128
  76. package/src/browser/prefetch/cache.ts +107 -56
  77. package/src/browser/prefetch/fetch.ts +204 -34
  78. package/src/browser/prefetch/queue.ts +6 -3
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +30 -7
  81. package/src/browser/react/NavigationProvider.tsx +283 -118
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  84. package/src/browser/react/filter-segment-order.ts +66 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +178 -8
  87. package/src/browser/react/location-state.ts +39 -14
  88. package/src/browser/react/use-action.ts +6 -15
  89. package/src/browser/react/use-handle.ts +17 -14
  90. package/src/browser/react/use-href.tsx +8 -1
  91. package/src/browser/react/use-link-status.ts +33 -8
  92. package/src/browser/react/use-navigation.ts +10 -5
  93. package/src/browser/react/use-params.ts +11 -11
  94. package/src/browser/react/use-reverse.ts +106 -0
  95. package/src/browser/react/use-router.ts +25 -3
  96. package/src/browser/react/use-search-params.ts +0 -5
  97. package/src/browser/react/use-segments.ts +11 -21
  98. package/src/browser/response-adapter.ts +99 -8
  99. package/src/browser/rsc-router.tsx +91 -24
  100. package/src/browser/scroll-restoration.ts +30 -17
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +214 -55
  103. package/src/browser/types.ts +80 -9
  104. package/src/browser/validate-redirect-origin.ts +43 -16
  105. package/src/build/collect-fallback-refs.ts +107 -0
  106. package/src/build/generate-manifest.ts +60 -35
  107. package/src/build/generate-route-types.ts +2 -1
  108. package/src/build/index.ts +8 -2
  109. package/src/build/prefix-tree-utils.ts +123 -0
  110. package/src/build/route-trie.ts +117 -14
  111. package/src/build/route-types/ast-route-extraction.ts +15 -8
  112. package/src/build/route-types/codegen.ts +16 -5
  113. package/src/build/route-types/include-resolution.ts +117 -23
  114. package/src/build/route-types/param-extraction.ts +6 -3
  115. package/src/build/route-types/per-module-writer.ts +22 -6
  116. package/src/build/route-types/router-processing.ts +55 -28
  117. package/src/build/route-types/scan-filter.ts +1 -1
  118. package/src/build/route-types/source-scan.ts +216 -0
  119. package/src/build/runtime-discovery.ts +9 -20
  120. package/src/cache/cache-error.ts +104 -0
  121. package/src/cache/cache-key-utils.ts +29 -13
  122. package/src/cache/cache-policy.ts +108 -34
  123. package/src/cache/cache-runtime.ts +224 -41
  124. package/src/cache/cache-scope.ts +188 -82
  125. package/src/cache/cache-tag.ts +103 -0
  126. package/src/cache/cf/cf-base64.ts +33 -0
  127. package/src/cache/cf/cf-cache-constants.ts +127 -0
  128. package/src/cache/cf/cf-cache-store.ts +1989 -378
  129. package/src/cache/cf/cf-cache-types.ts +349 -0
  130. package/src/cache/cf/cf-kv-utils.ts +46 -0
  131. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  132. package/src/cache/cf/index.ts +6 -16
  133. package/src/cache/document-cache.ts +89 -21
  134. package/src/cache/handle-snapshot.ts +70 -0
  135. package/src/cache/index.ts +10 -20
  136. package/src/cache/memory-segment-store.ts +136 -37
  137. package/src/cache/profile-registry.ts +46 -31
  138. package/src/cache/read-through-swr.ts +56 -12
  139. package/src/cache/segment-codec.ts +9 -17
  140. package/src/cache/tag-invalidation.ts +230 -0
  141. package/src/cache/types.ts +37 -100
  142. package/src/client.rsc.tsx +44 -21
  143. package/src/client.tsx +36 -61
  144. package/src/cloudflare/index.ts +11 -0
  145. package/src/cloudflare/tracing.ts +109 -0
  146. package/src/component-utils.ts +19 -0
  147. package/src/components/DefaultDocument.tsx +8 -2
  148. package/src/context-var.ts +18 -6
  149. package/src/decode-loader-results.ts +52 -0
  150. package/src/defer.ts +196 -0
  151. package/src/deps/ssr.ts +0 -1
  152. package/src/encode-kv.ts +49 -0
  153. package/src/errors.ts +30 -4
  154. package/src/escape-script.ts +52 -0
  155. package/src/handle.ts +31 -23
  156. package/src/handles/MetaTags.tsx +62 -19
  157. package/src/handles/Scripts.tsx +183 -0
  158. package/src/handles/breadcrumbs.ts +37 -8
  159. package/src/handles/is-thenable.ts +19 -0
  160. package/src/handles/meta.ts +51 -40
  161. package/src/handles/script.ts +244 -0
  162. package/src/host/cookie-handler.ts +9 -60
  163. package/src/host/errors.ts +0 -24
  164. package/src/host/index.ts +8 -2
  165. package/src/host/pattern-matcher.ts +23 -52
  166. package/src/host/router.ts +107 -99
  167. package/src/host/testing.ts +40 -27
  168. package/src/host/types.ts +37 -4
  169. package/src/host/utils.ts +1 -1
  170. package/src/href-client.ts +137 -22
  171. package/src/index.rsc.ts +96 -12
  172. package/src/index.ts +94 -14
  173. package/src/internal-debug.ts +11 -10
  174. package/src/loader-store.ts +500 -0
  175. package/src/loader.rsc.ts +20 -13
  176. package/src/loader.ts +12 -11
  177. package/src/missing-id-error.ts +68 -0
  178. package/src/outlet-context.ts +1 -1
  179. package/src/outlet-provider.tsx +1 -5
  180. package/src/prerender/param-hash.ts +16 -16
  181. package/src/prerender/store.ts +32 -37
  182. package/src/prerender.ts +61 -6
  183. package/src/redirect-origin.ts +100 -0
  184. package/src/regex-escape.ts +8 -0
  185. package/src/render-error-thrower.tsx +20 -0
  186. package/src/response-utils.ts +34 -0
  187. package/src/reverse.ts +65 -40
  188. package/src/root-error-boundary.tsx +1 -19
  189. package/src/route-content-wrapper.tsx +19 -77
  190. package/src/route-definition/dsl-helpers.ts +304 -309
  191. package/src/route-definition/helper-factories.ts +28 -140
  192. package/src/route-definition/helpers-types.ts +82 -55
  193. package/src/route-definition/index.ts +1 -2
  194. package/src/route-definition/redirect.ts +44 -11
  195. package/src/route-definition/resolve-handler-use.ts +12 -1
  196. package/src/route-definition/use-item-types.ts +29 -0
  197. package/src/route-map-builder.ts +0 -16
  198. package/src/route-types.ts +19 -46
  199. package/src/router/basename.ts +14 -0
  200. package/src/router/content-negotiation.ts +73 -25
  201. package/src/router/error-handling.ts +45 -18
  202. package/src/router/find-match.ts +44 -23
  203. package/src/router/handler-context.ts +27 -43
  204. package/src/router/instrument.ts +350 -0
  205. package/src/router/intercept-resolution.ts +39 -20
  206. package/src/router/lazy-includes.ts +10 -47
  207. package/src/router/loader-resolution.ts +155 -72
  208. package/src/router/logging.ts +0 -6
  209. package/src/router/manifest.ts +18 -29
  210. package/src/router/match-api.ts +9 -24
  211. package/src/router/match-context.ts +0 -22
  212. package/src/router/match-handlers.ts +58 -58
  213. package/src/router/match-middleware/background-revalidation.ts +40 -24
  214. package/src/router/match-middleware/cache-lookup.ts +159 -285
  215. package/src/router/match-middleware/cache-store.ts +64 -52
  216. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  217. package/src/router/match-middleware/segment-resolution.ts +0 -22
  218. package/src/router/match-pipelines.ts +1 -42
  219. package/src/router/match-result.ts +44 -74
  220. package/src/router/metrics.ts +0 -34
  221. package/src/router/middleware-types.ts +7 -134
  222. package/src/router/middleware.ts +247 -166
  223. package/src/router/navigation-snapshot.ts +0 -51
  224. package/src/router/params-util.ts +23 -0
  225. package/src/router/pattern-matching.ts +85 -94
  226. package/src/router/prefetch-cache-ttl.ts +51 -0
  227. package/src/router/prerender-match.ts +104 -65
  228. package/src/router/preview-match.ts +3 -1
  229. package/src/router/request-classification.ts +28 -62
  230. package/src/router/revalidation.ts +123 -73
  231. package/src/router/route-snapshot.ts +0 -1
  232. package/src/router/router-context.ts +3 -28
  233. package/src/router/router-interfaces.ts +83 -35
  234. package/src/router/router-options.ts +136 -5
  235. package/src/router/router-registry.ts +2 -5
  236. package/src/router/segment-resolution/fresh.ts +97 -84
  237. package/src/router/segment-resolution/helpers.ts +86 -6
  238. package/src/router/segment-resolution/loader-cache.ts +76 -39
  239. package/src/router/segment-resolution/revalidation.ts +272 -320
  240. package/src/router/segment-resolution/static-store.ts +19 -5
  241. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  242. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  243. package/src/router/segment-resolution.ts +5 -1
  244. package/src/router/segment-wrappers.ts +6 -5
  245. package/src/router/state-cookie-name.ts +33 -0
  246. package/src/router/substitute-pattern-params.ts +56 -0
  247. package/src/router/telemetry-otel.ts +161 -199
  248. package/src/router/telemetry.ts +96 -19
  249. package/src/router/timeout.ts +0 -20
  250. package/src/router/tracing.ts +206 -0
  251. package/src/router/trie-matching.ts +162 -64
  252. package/src/router/types.ts +9 -63
  253. package/src/router/url-params.ts +0 -5
  254. package/src/router.ts +110 -55
  255. package/src/rsc/handler-context.ts +3 -2
  256. package/src/rsc/handler.ts +264 -220
  257. package/src/rsc/helpers.ts +100 -6
  258. package/src/rsc/index.ts +2 -5
  259. package/src/rsc/json-route-result.ts +38 -0
  260. package/src/rsc/loader-fetch.ts +114 -38
  261. package/src/rsc/manifest-init.ts +28 -41
  262. package/src/rsc/origin-guard.ts +39 -25
  263. package/src/rsc/progressive-enhancement.ts +117 -11
  264. package/src/rsc/redirect-guard.ts +99 -0
  265. package/src/rsc/response-cache-serve.ts +238 -0
  266. package/src/rsc/response-error.ts +79 -12
  267. package/src/rsc/response-route-handler.ts +88 -188
  268. package/src/rsc/rsc-rendering.ts +98 -76
  269. package/src/rsc/runtime-warnings.ts +23 -10
  270. package/src/rsc/server-action.ts +281 -117
  271. package/src/rsc/ssr-setup.ts +16 -0
  272. package/src/rsc/transition-gate.ts +89 -0
  273. package/src/rsc/types.ts +23 -5
  274. package/src/runtime-env.ts +18 -0
  275. package/src/search-params.ts +35 -30
  276. package/src/segment-loader-promise.ts +31 -4
  277. package/src/segment-system.tsx +254 -143
  278. package/src/serialize.ts +243 -0
  279. package/src/server/context.ts +163 -51
  280. package/src/server/cookie-parse.ts +32 -0
  281. package/src/server/cookie-store.ts +80 -5
  282. package/src/server/handle-store.ts +21 -38
  283. package/src/server/loader-registry.ts +33 -42
  284. package/src/server/request-context.ts +287 -178
  285. package/src/ssr/index.tsx +21 -16
  286. package/src/static-handler.ts +10 -13
  287. package/src/testing/cache-status.ts +162 -0
  288. package/src/testing/collect-handle.ts +40 -0
  289. package/src/testing/dispatch.ts +701 -0
  290. package/src/testing/dom.entry.ts +22 -0
  291. package/src/testing/e2e/fixture.ts +188 -0
  292. package/src/testing/e2e/index.ts +128 -0
  293. package/src/testing/e2e/matchers.ts +35 -0
  294. package/src/testing/e2e/page-helpers.ts +272 -0
  295. package/src/testing/e2e/parity.ts +387 -0
  296. package/src/testing/e2e/server.ts +195 -0
  297. package/src/testing/flight-matchers.ts +97 -0
  298. package/src/testing/flight-normalize.ts +11 -0
  299. package/src/testing/flight-runtime.d.ts +57 -0
  300. package/src/testing/flight-tree.ts +682 -0
  301. package/src/testing/flight.entry.ts +52 -0
  302. package/src/testing/flight.ts +257 -0
  303. package/src/testing/generated-routes.ts +183 -0
  304. package/src/testing/index.ts +105 -0
  305. package/src/testing/internal/context.ts +371 -0
  306. package/src/testing/internal/flight-client-globals.ts +30 -0
  307. package/src/testing/internal/seed-vars.ts +54 -0
  308. package/src/testing/render-handler.ts +357 -0
  309. package/src/testing/render-route.tsx +581 -0
  310. package/src/testing/run-loader.ts +385 -0
  311. package/src/testing/run-middleware.ts +205 -0
  312. package/src/testing/run-transition-when.ts +164 -0
  313. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  314. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  315. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  316. package/src/testing/vitest-stubs/version.ts +5 -0
  317. package/src/testing/vitest.ts +305 -0
  318. package/src/theme/ThemeProvider.tsx +20 -58
  319. package/src/theme/ThemeScript.tsx +7 -9
  320. package/src/theme/constants.ts +52 -13
  321. package/src/theme/index.ts +0 -7
  322. package/src/theme/theme-context.ts +1 -5
  323. package/src/theme/theme-script.ts +22 -21
  324. package/src/theme/use-theme.ts +0 -3
  325. package/src/types/boundaries.ts +0 -35
  326. package/src/types/cache-types.ts +13 -4
  327. package/src/types/error-types.ts +30 -90
  328. package/src/types/global-namespace.ts +54 -41
  329. package/src/types/handler-context.ts +110 -62
  330. package/src/types/index.ts +3 -10
  331. package/src/types/loader-types.ts +11 -9
  332. package/src/types/request-scope.ts +112 -0
  333. package/src/types/route-config.ts +6 -50
  334. package/src/types/route-entry.ts +0 -6
  335. package/src/types/segments.ts +135 -14
  336. package/src/urls/include-helper.ts +9 -56
  337. package/src/urls/index.ts +1 -11
  338. package/src/urls/path-helper-types.ts +29 -12
  339. package/src/urls/path-helper.ts +17 -106
  340. package/src/urls/pattern-types.ts +36 -19
  341. package/src/urls/response-types.ts +22 -29
  342. package/src/urls/type-extraction.ts +58 -139
  343. package/src/urls/urls-function.ts +1 -19
  344. package/src/use-loader.tsx +292 -107
  345. package/src/vite/debug.ts +185 -0
  346. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  347. package/src/vite/discovery/discover-routers.ts +126 -85
  348. package/src/vite/discovery/discovery-errors.ts +194 -0
  349. package/src/vite/discovery/gate-state.ts +171 -0
  350. package/src/vite/discovery/prerender-collection.ts +96 -68
  351. package/src/vite/discovery/route-types-writer.ts +40 -84
  352. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  353. package/src/vite/discovery/state.ts +44 -0
  354. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  355. package/src/vite/index.ts +2 -0
  356. package/src/vite/inject-client-debug.ts +36 -0
  357. package/src/vite/plugin-types.ts +126 -8
  358. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  359. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  360. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  361. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  362. package/src/vite/plugins/expose-action-id.ts +48 -95
  363. package/src/vite/plugins/expose-id-utils.ts +88 -55
  364. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  365. package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
  366. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  367. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  368. package/src/vite/plugins/expose-internal-ids.ts +505 -486
  369. package/src/vite/plugins/performance-tracks.ts +26 -25
  370. package/src/vite/plugins/refresh-cmd.ts +1 -1
  371. package/src/vite/plugins/use-cache-transform.ts +73 -83
  372. package/src/vite/plugins/version-injector.ts +40 -29
  373. package/src/vite/plugins/version-plugin.ts +37 -40
  374. package/src/vite/plugins/virtual-entries.ts +39 -25
  375. package/src/vite/rango.ts +109 -118
  376. package/src/vite/router-discovery.ts +718 -119
  377. package/src/vite/utils/ast-handler-extract.ts +26 -35
  378. package/src/vite/utils/banner.ts +1 -1
  379. package/src/vite/utils/bundle-analysis.ts +10 -15
  380. package/src/vite/utils/client-chunks.ts +184 -0
  381. package/src/vite/utils/directive-prologue.ts +40 -0
  382. package/src/vite/utils/forward-user-plugins.ts +171 -0
  383. package/src/vite/utils/manifest-utils.ts +4 -59
  384. package/src/vite/utils/package-resolution.ts +20 -52
  385. package/src/vite/utils/prerender-utils.ts +54 -39
  386. package/src/vite/utils/shared-utils.ts +90 -41
  387. package/src/browser/action-response-classifier.ts +0 -99
  388. package/src/browser/react/use-client-cache.ts +0 -58
  389. package/src/browser/shallow.ts +0 -40
  390. package/src/handles/index.ts +0 -7
  391. package/src/network-error-thrower.tsx +0 -23
  392. package/src/router/middleware-cookies.ts +0 -55
@@ -8,12 +8,11 @@
8
8
  */
9
9
 
10
10
  import { createElement } from "react";
11
- import { RouteNotFoundError } from "../errors.js";
11
+ import { isRouteNotFoundError } from "../errors.js";
12
12
  import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
13
13
  import {
14
14
  runWithRequestContext,
15
15
  setRequestContextParams,
16
- requireRequestContext,
17
16
  getRequestContext,
18
17
  _getRequestContext,
19
18
  createRequestContext,
@@ -31,7 +30,11 @@ import {
31
30
  interceptRedirectForPartial,
32
31
  buildRouteMiddlewareEntries,
33
32
  } from "./helpers.js";
34
- import { isWebSocketUpgradeResponse } from "../response-utils.js";
33
+ import { guardOutgoingRedirect } from "./redirect-guard.js";
34
+ import {
35
+ isWebSocketUpgradeResponse,
36
+ appendVaryAccept,
37
+ } from "../response-utils.js";
35
38
  import {
36
39
  handleResponseRoute,
37
40
  type ResponseRouteMatch,
@@ -45,8 +48,6 @@ import {
45
48
  createReverseFunction,
46
49
  stripInternalParams,
47
50
  } from "../router/handler-context.js";
48
- import { getRouterContext } from "../router/router-context.js";
49
- import { resolveSink, safeEmit } from "../router/telemetry.js";
50
51
  import { contextSet } from "../context-var.js";
51
52
  import {
52
53
  hasCachedManifest,
@@ -57,6 +58,8 @@ import {
57
58
  getRouterTrie,
58
59
  } from "../route-map-builder.js";
59
60
  import type { HandlerContext } from "./handler-context.js";
61
+ import type { CacheErrorCategory } from "../cache/cache-error.js";
62
+ import type { SegmentCacheStore } from "../cache/types.js";
60
63
  import { buildRouterTrieFromUrlpatterns } from "./manifest-init.js";
61
64
  import { handleProgressiveEnhancement } from "./progressive-enhancement.js";
62
65
  import {
@@ -65,7 +68,10 @@ import {
65
68
  type ActionContinuation,
66
69
  } from "./server-action.js";
67
70
  import { handleLoaderFetch } from "./loader-fetch.js";
68
- import { checkRequestOrigin, type OriginCheckPhase } from "./origin-guard.js";
71
+ import {
72
+ checkRequestOrigin,
73
+ ORIGIN_CHECK_PHASE_BY_MODE,
74
+ } from "./origin-guard.js";
69
75
  import { handleRscRendering } from "./rsc-rendering.js";
70
76
  import {
71
77
  withTimeout,
@@ -78,10 +84,12 @@ import {
78
84
  appendMetric,
79
85
  buildMetricsTiming,
80
86
  } from "../router/metrics.js";
87
+ import { observePhase, observeEvent, PHASES } from "../router/instrument.js";
81
88
  import {
82
89
  startSSRSetup,
83
90
  getSSRSetup,
84
91
  mayNeedSSR,
92
+ isRscRequest,
85
93
  SSR_SETUP_VAR,
86
94
  } from "./ssr-setup.js";
87
95
  import {
@@ -123,12 +131,35 @@ import {
123
131
  * });
124
132
  * ```
125
133
  */
134
+
135
+ /**
136
+ * Response that tells the client to do a full document navigation. Shared by
137
+ * the terminal reload plans (version-mismatch and app-switch): an empty 200
138
+ * carrying X-RSC-Reload, which the client turns into window.location.href.
139
+ */
140
+ function createReloadResponse(reloadUrl: string) {
141
+ return createResponseWithMergedHeaders(null, {
142
+ status: 200,
143
+ headers: {
144
+ "X-RSC-Reload": reloadUrl,
145
+ "content-type": "text/x-component;charset=utf-8",
146
+ },
147
+ });
148
+ }
149
+
126
150
  export function createRSCHandler<
127
151
  TEnv = unknown,
128
152
  TRoutes extends Record<string, string> = Record<string, string>,
129
153
  >(options: CreateRSCHandlerOptions<TEnv, TRoutes>) {
130
154
  const { router, version = VERSION, nonce: nonceProvider } = options;
131
155
 
156
+ // Handler-owned registry of explicit per-scope stores from cache({ store }).
157
+ // Lives in the closure so it is scoped per handler (multi-router deployments
158
+ // get separate registries) and accumulates every explicit store this handler
159
+ // resolves across requests. updateTag()/revalidateTag() iterate it to reach
160
+ // stores not covered by the app-level ctx._cacheStore.
161
+ const explicitTaggedStores = new Set<SegmentCacheStore>();
162
+
132
163
  // Use provided deps or default to @vitejs/plugin-rsc/rsc exports
133
164
  const deps = options.deps ?? rscDeps;
134
165
  const {
@@ -214,24 +245,16 @@ export function createRSCHandler<
214
245
  metadata: { timeout: true, phase, durationMs },
215
246
  });
216
247
 
217
- try {
218
- const routerCtx = getRouterContext();
219
- if (routerCtx?.telemetry) {
220
- safeEmit(resolveSink(routerCtx.telemetry), {
221
- type: "request.timeout" as const,
222
- timestamp: performance.now(),
223
- requestId: routerCtx.requestId,
224
- phase,
225
- pathname: url.pathname,
226
- routeKey,
227
- actionId,
228
- durationMs,
229
- customHandler: !!router.onTimeout,
230
- });
231
- }
232
- } catch {
233
- // Router context may not be available
234
- }
248
+ observeEvent({
249
+ type: "request.timeout",
250
+ timestamp: performance.now(),
251
+ phase,
252
+ pathname: url.pathname,
253
+ routeKey,
254
+ actionId,
255
+ durationMs,
256
+ customHandler: !!router.onTimeout,
257
+ });
235
258
 
236
259
  if (router.onTimeout) {
237
260
  try {
@@ -263,16 +286,27 @@ export function createRSCHandler<
263
286
  function createRedirectFlightResponse(
264
287
  redirectUrl: string,
265
288
  locationState?: Record<string, unknown>,
289
+ external?: boolean,
266
290
  ): Response {
267
291
  const redirectPayload: RscPayload = {
268
292
  metadata: {
269
293
  pathname: redirectUrl,
270
294
  segments: [],
271
- redirect: { url: redirectUrl },
295
+ redirect: { url: redirectUrl, ...(external && { external: true }) },
272
296
  ...(locationState && { locationState }),
273
297
  },
274
298
  };
275
- const rscStream = renderToReadableStream<RscPayload>(redirectPayload);
299
+ const rscStream = renderToReadableStream<RscPayload>(redirectPayload, {
300
+ onError: (error: unknown) => {
301
+ const reqCtx = _getRequestContext<TEnv>();
302
+ if (!reqCtx) return;
303
+ callOnError(error, "rendering", {
304
+ request: reqCtx.request,
305
+ url: reqCtx.url,
306
+ env: reqCtx.env,
307
+ });
308
+ },
309
+ });
276
310
  return createResponseWithMergedHeaders(rscStream, {
277
311
  status: 200,
278
312
  headers: { "content-type": "text/x-component;charset=utf-8" },
@@ -353,7 +387,7 @@ export function createRSCHandler<
353
387
  // Resolve cache store configuration
354
388
  // Priority: options.cache (handler override) > router.cache (router default)
355
389
  // Store is enabled only if: config provided, enabled, and no ?__no_cache query param
356
- let cacheStore = undefined;
390
+ let cacheStore: SegmentCacheStore | undefined;
357
391
  const cacheOption = options.cache ?? router.cache;
358
392
  if (cacheOption && !url.searchParams.has("__no_cache")) {
359
393
  const cacheConfig =
@@ -420,9 +454,12 @@ export function createRSCHandler<
420
454
  url,
421
455
  variables,
422
456
  cacheStore,
457
+ explicitTaggedStores,
423
458
  cacheProfiles: router.cacheProfiles,
424
459
  executionContext: executionCtx,
425
460
  themeConfig: router.themeConfig,
461
+ stateCookieName: router.resolvedStateCookieName,
462
+ version,
426
463
  });
427
464
  if (earlyMetricsStore) {
428
465
  requestContext._debugPerformance = true;
@@ -432,7 +469,7 @@ export function createRSCHandler<
432
469
  // can surface non-fatal errors through the router's onError callback.
433
470
  requestContext._reportBackgroundError = (
434
471
  error: unknown,
435
- category: string,
472
+ category: CacheErrorCategory,
436
473
  ) => {
437
474
  callOnError(error, "cache", {
438
475
  request,
@@ -465,81 +502,104 @@ export function createRSCHandler<
465
502
  // Store basename on request context (scoped per-request via existing ALS)
466
503
  requestContext._basename = router.basename;
467
504
 
468
- return runWithRequestContext(requestContext, async () => {
469
- // Core handler logic (wrapped by middleware)
470
- const coreHandler = async (): Promise<Response> => {
471
- return coreRequestHandler(request, env, url, variables, nonce);
472
- };
473
-
474
- // Execute middleware chain if any, otherwise call core handler directly
475
- let response: Response;
476
- if (matchedMiddleware.length > 0) {
477
- const mwResponse = await executeMiddleware(
478
- matchedMiddleware,
479
- request,
480
- env,
481
- variables,
482
- coreHandler,
483
- createReverseFunction(getRequiredRouteMap()),
484
- );
505
+ // Resolved span tracing for this request (read at each traced phase).
506
+ requestContext._tracing = router.tracing;
507
+
508
+ // The "rango.request" span is opened inside the request context so the
509
+ // Cloudflare runner can read executionContext.tracing, and so every nested
510
+ // phase span (and the platform's automatic KV/D1/fetch spans) nests under
511
+ // it. Construction-bound: the span ends when the Response is built, never
512
+ // wrapping the streamed body. metric:false — handler:total is metered
513
+ // directly below (a grand total incl. the pre-context bootstrap timings).
514
+ // When tracing is off this is a direct pass-through.
515
+ return runWithRequestContext(requestContext, () =>
516
+ observePhase(PHASES.request, async (span) => {
517
+ span.setAttribute("http.method", request.method);
518
+ // The matched route template is not known until match() runs later, so
519
+ // emit the concrete path as url.path (low-level), NOT http.route — the
520
+ // latter is reserved for the low-cardinality template (OTel convention).
521
+ span.setAttribute("url.path", url.pathname);
522
+
523
+ // Core handler logic (wrapped by middleware)
524
+ const coreHandler = async (): Promise<Response> => {
525
+ return coreRequestHandler(request, env, url, variables, nonce);
526
+ };
485
527
 
486
- if (
487
- url.searchParams.has("_rsc_partial") ||
488
- url.searchParams.has("_rsc_action")
489
- ) {
490
- const intercepted = interceptRedirectForPartial(
491
- mwResponse,
492
- createRedirectFlightResponse,
528
+ // Execute middleware chain if any, otherwise call core handler directly
529
+ let response: Response;
530
+ if (matchedMiddleware.length > 0) {
531
+ const mwResponse = await executeMiddleware(
532
+ matchedMiddleware,
533
+ request,
534
+ env,
535
+ variables,
536
+ coreHandler,
537
+ createReverseFunction(getRequiredRouteMap()),
493
538
  );
494
- response = intercepted ?? finalizeResponse(mwResponse);
539
+
540
+ if (
541
+ url.searchParams.has("_rsc_partial") ||
542
+ url.searchParams.has("_rsc_action")
543
+ ) {
544
+ const intercepted = interceptRedirectForPartial(
545
+ mwResponse,
546
+ createRedirectFlightResponse,
547
+ );
548
+ response = intercepted ?? finalizeResponse(mwResponse);
549
+ } else {
550
+ response = finalizeResponse(mwResponse);
551
+ }
495
552
  } else {
496
- response = finalizeResponse(mwResponse);
553
+ response = await coreHandler();
497
554
  }
498
- } else {
499
- response = await coreHandler();
500
- }
501
555
 
502
- // Finalize metrics after all middleware (including post-next work)
503
- // has completed so :post spans are captured in the timeline.
504
- // Handler timing parts are always emitted (even without debug metrics)
505
- // so non-debug requests still get bootstrap Server-Timing entries.
506
- const handlerTimingArr: string[] = variables.__handlerTiming || [];
507
- // Preserve any existing Server-Timing set by response routes or middleware
508
- const existingTiming = response.headers.get("Server-Timing");
509
- const timingParts = existingTiming
510
- ? [existingTiming, ...handlerTimingArr]
511
- : [...handlerTimingArr];
512
-
513
- const metricsStore = requestContext._metricsStore;
514
- if (metricsStore) {
515
- // When the store was created at handler start (earlyMetricsStore),
516
- // handler:total covers the full request. When ctx.debugPerformance()
517
- // created the store mid-request, use its requestStart to avoid a
518
- // negative startTime offset.
519
- const totalStart = earlyMetricsStore
520
- ? handlerStart
521
- : metricsStore.requestStart;
522
- appendMetric(
523
- metricsStore,
524
- "handler:total",
525
- totalStart,
526
- performance.now() - totalStart,
527
- );
528
- const metricsTiming = buildMetricsTiming(
529
- request.method,
530
- url.pathname,
531
- metricsStore,
532
- );
533
- if (metricsTiming) timingParts.push(metricsTiming);
534
- }
556
+ // Finalize metrics after all middleware (including post-next work)
557
+ // has completed so :post spans are captured in the timeline.
558
+ // Handler timing parts are always emitted (even without debug metrics)
559
+ // so non-debug requests still get bootstrap Server-Timing entries.
560
+ const handlerTimingArr: string[] = variables.__handlerTiming || [];
561
+ // Preserve any existing Server-Timing set by response routes or middleware
562
+ const existingTiming = response.headers.get("Server-Timing");
563
+ const timingParts = existingTiming
564
+ ? [existingTiming, ...handlerTimingArr]
565
+ : [...handlerTimingArr];
566
+
567
+ const metricsStore = requestContext._metricsStore;
568
+ if (metricsStore) {
569
+ // When the store was created at handler start (earlyMetricsStore),
570
+ // handler:total covers the full request. When ctx.debugPerformance()
571
+ // created the store mid-request, use its requestStart to avoid a
572
+ // negative startTime offset.
573
+ const totalStart = earlyMetricsStore
574
+ ? handlerStart
575
+ : metricsStore.requestStart;
576
+ appendMetric(
577
+ metricsStore,
578
+ "handler:total",
579
+ totalStart,
580
+ performance.now() - totalStart,
581
+ );
582
+ const metricsTiming = buildMetricsTiming(
583
+ request.method,
584
+ url.pathname,
585
+ metricsStore,
586
+ );
587
+ if (metricsTiming) timingParts.push(metricsTiming);
588
+ }
535
589
 
536
- const fullTiming = timingParts.join(", ");
537
- if (fullTiming && !isWebSocketUpgradeResponse(response)) {
538
- response.headers.set("Server-Timing", fullTiming);
539
- }
590
+ const fullTiming = timingParts.join(", ");
591
+ if (fullTiming && !isWebSocketUpgradeResponse(response)) {
592
+ response.headers.set("Server-Timing", fullTiming);
593
+ }
540
594
 
541
- return response;
542
- });
595
+ // Single open-redirect chokepoint: every response (PE, full-page,
596
+ // middleware short-circuit, response-route) funnels through here, so
597
+ // guarding browser-followed (3xx) redirects once covers them all and any
598
+ // future redirect exit. Soft SPA/Flight redirects are 200/204 and pass
599
+ // through untouched (validated client-side instead).
600
+ return guardOutgoingRedirect(response, url.origin, router.basename);
601
+ }),
602
+ );
543
603
  };
544
604
 
545
605
  // Core request handling logic (separated for middleware wrapping).
@@ -596,10 +656,7 @@ export function createRSCHandler<
596
656
  routerId: router.id,
597
657
  });
598
658
  } catch (error) {
599
- if (
600
- error instanceof RouteNotFoundError ||
601
- (error instanceof Error && error.name === "RouteNotFoundError")
602
- ) {
659
+ if (isRouteNotFoundError(error)) {
603
660
  // Let the render path handle 404 — match()/matchPartial() will
604
661
  // re-throw RouteNotFoundError and the catch block in
605
662
  // executeRenderWithMiddleware renders the not-found page.
@@ -640,24 +697,18 @@ export function createRSCHandler<
640
697
  console.log(
641
698
  `[RSC] Version mismatch: client=${url.searchParams.get("_rsc_v")}, server=${version}. Forcing reload.`,
642
699
  );
643
- return createResponseWithMergedHeaders(null, {
644
- status: 200,
645
- headers: {
646
- "X-RSC-Reload": plan.reloadUrl,
647
- "content-type": "text/x-component;charset=utf-8",
648
- },
649
- });
700
+ return createReloadResponse(plan.reloadUrl);
701
+ }
702
+
703
+ if (plan.mode === "app-switch") {
704
+ // Cross-app SPA navigation crossed a host-router app boundary. Force a
705
+ // real document navigation so the target app's document is re-established
706
+ // (stylesheets, theme, warmup, prefetch-TTL). See request-classification.
707
+ return createReloadResponse(plan.reloadUrl);
650
708
  }
651
709
 
652
710
  // ---- 3. Origin guard (gate for action/loader/PE modes) ----
653
- const originPhase: OriginCheckPhase | null =
654
- plan.mode === "action"
655
- ? "action"
656
- : plan.mode === "loader"
657
- ? "loader"
658
- : plan.mode === "pe-render"
659
- ? "pe-form"
660
- : null;
711
+ const originPhase = ORIGIN_CHECK_PHASE_BY_MODE[plan.mode];
661
712
  if (originPhase) {
662
713
  const originResult = await checkRequestOrigin(
663
714
  request,
@@ -685,23 +736,15 @@ export function createRSCHandler<
685
736
  },
686
737
  });
687
738
 
688
- try {
689
- const routerCtx = getRouterContext();
690
- if (routerCtx?.telemetry) {
691
- safeEmit(resolveSink(routerCtx.telemetry), {
692
- type: "request.origin-rejected" as const,
693
- timestamp: performance.now(),
694
- requestId: routerCtx.requestId,
695
- method: request.method,
696
- pathname: url.pathname,
697
- phase: originPhase,
698
- origin: request.headers.get("origin"),
699
- host: request.headers.get("host"),
700
- });
701
- }
702
- } catch {
703
- // Router context may not be available
704
- }
739
+ observeEvent({
740
+ type: "request.origin-rejected",
741
+ timestamp: performance.now(),
742
+ method: request.method,
743
+ pathname: url.pathname,
744
+ phase: originPhase,
745
+ origin: request.headers.get("origin"),
746
+ host: request.headers.get("host"),
747
+ });
705
748
 
706
749
  return originResult;
707
750
  }
@@ -731,11 +774,11 @@ export function createRSCHandler<
731
774
  nonce: string | undefined,
732
775
  ): Promise<Response> {
733
776
  // Common setup
734
- const handleStore = requireRequestContext()._handleStore;
777
+ const handleStore = getRequestContext()._handleStore;
735
778
 
736
779
  // Wire up error reporting for late streaming-handle failures
737
780
  handleStore.onError = (error: Error) => {
738
- const reqCtx = requireRequestContext();
781
+ const reqCtx = getRequestContext();
739
782
  callOnError(error, "handler", {
740
783
  request,
741
784
  url,
@@ -743,23 +786,15 @@ export function createRSCHandler<
743
786
  params: reqCtx.params as Record<string, string>,
744
787
  handledByBoundary: true,
745
788
  });
746
- try {
747
- const routerCtx = getRouterContext();
748
- if (routerCtx?.telemetry) {
749
- safeEmit(resolveSink(routerCtx.telemetry), {
750
- type: "handler.error" as const,
751
- timestamp: performance.now(),
752
- requestId: routerCtx.requestId,
753
- error,
754
- handledByBoundary: true,
755
- pathname: url.pathname,
756
- routeKey: reqCtx._routeName,
757
- params: reqCtx.params as Record<string, string>,
758
- });
759
- }
760
- } catch {
761
- // Router context may not be available (e.g. prerender path)
762
- }
789
+ observeEvent({
790
+ type: "handler.error",
791
+ timestamp: performance.now(),
792
+ error,
793
+ handledByBoundary: true,
794
+ pathname: url.pathname,
795
+ routeKey: reqCtx._routeName,
796
+ params: reqCtx.params as Record<string, string>,
797
+ });
763
798
  };
764
799
 
765
800
  // Set route params early so all execution paths can access ctx.params.
@@ -767,7 +802,7 @@ export function createRSCHandler<
767
802
  // instead of calling resolveRoute again.
768
803
  if (plan.mode !== "redirect") {
769
804
  setRequestContextParams(plan.route.params, plan.route.routeKey);
770
- requireRequestContext()._classifiedRoute = plan.route;
805
+ getRequestContext()._classifiedRoute = plan.route;
771
806
  }
772
807
 
773
808
  const routeReverse = createReverseFunction(getRequiredRouteMap());
@@ -808,7 +843,9 @@ export function createRSCHandler<
808
843
  }
809
844
  const response = responseOutcome.result;
810
845
  if (plan.negotiated && !isWebSocketUpgradeResponse(response)) {
811
- response.headers.append("Vary", "Accept");
846
+ // handleResponseRoute (callHandlerWithVary) already appends Vary: Accept
847
+ // for negotiated responses; dedup so we don't emit Vary: Accept, Accept.
848
+ appendVaryAccept(response);
812
849
  }
813
850
  return response;
814
851
  }
@@ -816,14 +853,28 @@ export function createRSCHandler<
816
853
  // SSR setup: kick off in parallel for modes that need HTML rendering.
817
854
  // Placed after response-route short-circuit so response/mime routes
818
855
  // never pay for SSR work.
819
- if (plan.mode !== "loader" && mayNeedSSR(request, url)) {
856
+ //
857
+ // Only kick off when the request will actually render HTML, so the
858
+ // eager loadSSRModule() + user resolveStreaming() are not started (and
859
+ // never consumed) for a request that returns an RSC stream — that wasted
860
+ // work also leaves an orphaned Promise.all that can reject (D7). PE form
861
+ // submissions always render HTML (handleProgressiveEnhancement renders via
862
+ // getSSRSetup regardless of Accept). For full/partial-render and action,
863
+ // the render-time HTML decision is exactly !isRscRequest — mayNeedSSR is
864
+ // the coarse transport pre-filter, isRscRequest is the precise Accept call
865
+ // (it, unlike mayNeedSSR, treats a MISSING Accept as RSC). Both must pass.
866
+ const willRenderHtml =
867
+ plan.mode === "pe-render" ||
868
+ (mayNeedSSR(request, url) &&
869
+ !isRscRequest(request, url, plan.mode === "partial-render"));
870
+ if (plan.mode !== "loader" && willRenderHtml) {
820
871
  variables[SSR_SETUP_VAR] = startSSRSetup(
821
872
  handlerCtx,
822
873
  request,
823
874
  env,
824
875
  url,
825
876
  router.debugPerformance
826
- ? () => requireRequestContext()._metricsStore
877
+ ? () => getRequestContext()._metricsStore
827
878
  : undefined,
828
879
  );
829
880
  }
@@ -864,14 +915,20 @@ export function createRSCHandler<
864
915
  if (plan.mode === "action") {
865
916
  let actionContinuation: ActionContinuation | undefined;
866
917
  try {
918
+ // Instrument the action execution as its own phase (action:<actionId> +
919
+ // rango.action), so a POST shows the mutation time AND which action ran,
920
+ // not just the downstream revalidation render. The action's own
921
+ // loaders/fetches nest under rango.action.
867
922
  const actionOutcome = await withTimeout(
868
- executeServerAction(
869
- handlerCtx,
870
- request,
871
- env,
872
- url,
873
- plan.actionId,
874
- handleStore,
923
+ observePhase(PHASES.action(plan.actionId), () =>
924
+ executeServerAction(
925
+ handlerCtx,
926
+ request,
927
+ env,
928
+ url,
929
+ plan.actionId,
930
+ handleStore,
931
+ ),
875
932
  ),
876
933
  router.timeouts.actionMs,
877
934
  "action",
@@ -924,47 +981,17 @@ export function createRSCHandler<
924
981
  );
925
982
  }
926
983
 
927
- // ---- Full render / Partial render (or PE that fell through) ----
928
- if (plan.mode === "full-render" || plan.mode === "partial-render") {
929
- const isPartial = plan.mode === "partial-render";
930
- return executeRenderWithMiddleware(
931
- plan.route.routeMiddleware,
932
- plan.negotiated,
933
- plan.route.routeKey,
934
- routeReverse,
935
- request,
936
- env,
937
- url,
938
- variables,
939
- nonce,
940
- handleStore,
941
- isPartial,
942
- );
943
- }
944
-
945
- // PE that fell through (handleProgressiveEnhancement returned null)
946
- // falls back to full render
947
- if (plan.mode === "pe-render") {
948
- return executeRenderWithMiddleware(
949
- plan.route.routeMiddleware,
950
- false,
951
- plan.route.routeKey,
952
- routeReverse,
953
- request,
954
- env,
955
- url,
956
- variables,
957
- nonce,
958
- handleStore,
959
- false,
960
- );
961
- }
962
-
963
- // Redirect plan that wasn't handled above (full-page redirect — let
964
- // the pipeline handle it via match() which returns { redirect: url })
984
+ // Full render, partial render, fallen-through PE, and full-page redirect all
985
+ // render through the same middleware-wrapped path. Only full/partial-render
986
+ // carry negotiation + the partial flag; pe/redirect render plainly.
987
+ const isPartial = plan.mode === "partial-render";
988
+ const negotiated =
989
+ plan.mode === "full-render" || plan.mode === "partial-render"
990
+ ? plan.negotiated
991
+ : false;
965
992
  return executeRenderWithMiddleware(
966
993
  plan.route.routeMiddleware,
967
- false,
994
+ negotiated,
968
995
  plan.route.routeKey,
969
996
  routeReverse,
970
997
  request,
@@ -973,7 +1000,7 @@ export function createRSCHandler<
973
1000
  variables,
974
1001
  nonce,
975
1002
  handleStore,
976
- false,
1003
+ isPartial,
977
1004
  );
978
1005
  }
979
1006
 
@@ -990,7 +1017,7 @@ export function createRSCHandler<
990
1017
  url: URL,
991
1018
  variables: Record<string, any>,
992
1019
  nonce: string | undefined,
993
- handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
1020
+ handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
994
1021
  isPartial: boolean,
995
1022
  actionContinuation?: ActionContinuation,
996
1023
  ): Promise<Response> {
@@ -1018,16 +1045,27 @@ export function createRSCHandler<
1018
1045
  );
1019
1046
  }
1020
1047
  if (negotiated && !isWebSocketUpgradeResponse(response)) {
1021
- response.headers.append("Vary", "Accept");
1048
+ // handleRscRendering bakes `accept` into the RSC response's Vary list;
1049
+ // dedup so the negotiated append does not list accept twice.
1050
+ appendVaryAccept(response);
1022
1051
  }
1023
1052
  return response;
1024
1053
  } catch (error) {
1025
1054
  // Check if middleware/handler returned Response
1026
1055
  if (error instanceof Response) {
1056
+ // An action revalidation render is delivered to the client over the
1057
+ // same Flight-parsing path as a partial navigation, so a Response
1058
+ // thrown during it must be converted exactly like a partial one
1059
+ // (raw 200 -> hard-nav hint, 3xx -> Flight redirect). Without this,
1060
+ // the no-middleware path returns the raw Response (the with-middleware
1061
+ // path is already covered by the isPartial || actionContinuation
1062
+ // guard below).
1063
+ const treatAsPartial = isPartial || actionContinuation != null;
1064
+
1027
1065
  // During partial (client-side navigation), a 200 Response from a handler
1028
1066
  // means the route serves raw content (JSON, text, etc.), not JSX.
1029
1067
  // Signal the browser to hard-navigate so it renders the raw response.
1030
- if (isPartial && error.status === 200) {
1068
+ if (treatAsPartial && error.status === 200) {
1031
1069
  console.warn(
1032
1070
  `[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
1033
1071
  `Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
@@ -1041,7 +1079,7 @@ export function createRSCHandler<
1041
1079
  });
1042
1080
  }
1043
1081
 
1044
- if (isPartial) {
1082
+ if (treatAsPartial) {
1045
1083
  const intercepted = interceptRedirectForPartial(
1046
1084
  error,
1047
1085
  createRedirectFlightResponse,
@@ -1053,10 +1091,7 @@ export function createRSCHandler<
1053
1091
  }
1054
1092
 
1055
1093
  // Render 404 page for unmatched routes
1056
- const isRouteNotFound =
1057
- error instanceof RouteNotFoundError ||
1058
- (error instanceof Error && error.name === "RouteNotFoundError");
1059
- if (isRouteNotFound) {
1094
+ if (isRouteNotFoundError(error)) {
1060
1095
  callOnError(error, "routing", {
1061
1096
  request,
1062
1097
  url,
@@ -1087,13 +1122,23 @@ export function createRSCHandler<
1087
1122
  segments: [notFoundSegment],
1088
1123
  matched: [],
1089
1124
  diff: [],
1125
+ // Shape parity with buildFullPayload (rsc-rendering.ts): the 404
1126
+ // payload carries params/resolvedIds/prefetchCacheTTL the same way a
1127
+ // matched full render does. resolvedIds mirrors the rendered segment
1128
+ // list (the single notFound segment) like the error-boundary path
1129
+ // (match-api.ts) does for its boundary segment.
1130
+ resolvedIds: [notFoundSegment.id],
1131
+ params: {},
1090
1132
  isPartial: false,
1091
1133
  rootLayout: router.rootLayout,
1092
1134
  handles: handleStore.stream(),
1093
1135
  version,
1136
+ prefetchCacheTTL: router.prefetchCacheTTL,
1137
+ stateCookieName: router.resolvedStateCookieName,
1094
1138
  themeConfig: router.themeConfig,
1095
1139
  warmupEnabled: router.warmupEnabled,
1096
- initialTheme: requireRequestContext().theme,
1140
+ strictMode: router.strictMode,
1141
+ initialTheme: getRequestContext().theme,
1097
1142
  },
1098
1143
  };
1099
1144
 
@@ -1103,16 +1148,15 @@ export function createRSCHandler<
1103
1148
  },
1104
1149
  });
1105
1150
 
1106
- const isRscRequest =
1107
- isPartial ||
1108
- (!request.headers.get("accept")?.includes("text/html") &&
1109
- !url.searchParams.has("__html")) ||
1110
- url.searchParams.has("__rsc");
1111
-
1112
- if (isRscRequest) {
1151
+ if (isRscRequest(request, url, isPartial)) {
1113
1152
  return createResponseWithMergedHeaders(rscStream, {
1114
1153
  status: 404,
1115
- headers: { "content-type": "text/x-component;charset=utf-8" },
1154
+ headers: {
1155
+ "content-type": "text/x-component;charset=utf-8",
1156
+ // Router identity for the client's pre-decode integrity check; a
1157
+ // same-app 404 matches and applies in place. See response-adapter.
1158
+ "X-RSC-Router-Id": router.id,
1159
+ },
1116
1160
  });
1117
1161
  }
1118
1162
 
@@ -1121,7 +1165,7 @@ export function createRSCHandler<
1121
1165
  request,
1122
1166
  env,
1123
1167
  url,
1124
- requireRequestContext()._metricsStore,
1168
+ getRequestContext()._metricsStore,
1125
1169
  );
1126
1170
  const htmlStream = await ssrModule.renderHTML(rscStream, {
1127
1171
  nonce,