@rangojs/router 0.22.1 → 0.24.0

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 (259) hide show
  1. package/dist/bin/rango.js +10 -4
  2. package/dist/testing/vitest.js +31 -1
  3. package/dist/types/browser/defer.d.ts +48 -0
  4. package/dist/types/browser/document-revealed.d.ts +17 -0
  5. package/dist/types/browser/event-controller.d.ts +7 -1
  6. package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
  7. package/dist/types/browser/navigation-transaction.d.ts +21 -7
  8. package/dist/types/browser/partial-update.d.ts +39 -8
  9. package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
  10. package/dist/types/browser/react/context.d.ts +3 -2
  11. package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
  12. package/dist/types/browser/response-adapter.d.ts +7 -0
  13. package/dist/types/browser/types.d.ts +24 -10
  14. package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
  15. package/dist/types/build/generate-route-types.d.ts +1 -1
  16. package/dist/types/build/route-types/router-processing.d.ts +1 -1
  17. package/dist/types/build/route-types/scan-filter.d.ts +22 -2
  18. package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
  19. package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
  20. package/dist/types/cache/document-cache-policy.d.ts +24 -0
  21. package/dist/types/cache/document-cache.d.ts +2 -10
  22. package/dist/types/client-urls/client-urls.d.ts +1 -1
  23. package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
  24. package/dist/types/client-urls/navigation.d.ts +0 -37
  25. package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
  26. package/dist/types/client-urls/server-projection.d.ts +29 -1
  27. package/dist/types/client-urls/types.d.ts +34 -69
  28. package/dist/types/client.d.ts +0 -1
  29. package/dist/types/context-var.d.ts +12 -0
  30. package/dist/types/errors.d.ts +15 -0
  31. package/dist/types/handles/deferred-resolution.d.ts +2 -1
  32. package/dist/types/handles/pushed-by.d.ts +31 -0
  33. package/dist/types/index.d.ts +4 -0
  34. package/dist/types/index.rsc.d.ts +2 -0
  35. package/dist/types/loader-store.d.ts +8 -0
  36. package/dist/types/prefetch-markers.d.ts +11 -0
  37. package/dist/types/response-utils.d.ts +7 -0
  38. package/dist/types/reverse.d.ts +8 -8
  39. package/dist/types/route-definition/helpers-types.d.ts +27 -0
  40. package/dist/types/route-definition/rewrite.d.ts +56 -0
  41. package/dist/types/route-map-builder.d.ts +7 -2
  42. package/dist/types/router/basename.d.ts +7 -0
  43. package/dist/types/router/handler-context.d.ts +1 -5
  44. package/dist/types/router/internal-params.d.ts +8 -0
  45. package/dist/types/router/is-action.d.ts +2 -1
  46. package/dist/types/router/middleware-types.d.ts +15 -0
  47. package/dist/types/router/revalidation.d.ts +1 -2
  48. package/dist/types/router/route-trie-builder.d.ts +10 -0
  49. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
  50. package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
  51. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
  52. package/dist/types/rsc/full-payload.d.ts +11 -0
  53. package/dist/types/rsc/handler.d.ts +33 -0
  54. package/dist/types/rsc/helpers.d.ts +8 -0
  55. package/dist/types/rsc/loader-fetch.d.ts +16 -0
  56. package/dist/types/rsc/rewrite-passes.d.ts +74 -0
  57. package/dist/types/rsc/ssr-setup.d.ts +3 -1
  58. package/dist/types/server/context.d.ts +21 -1
  59. package/dist/types/server/handle-store.d.ts +9 -1
  60. package/dist/types/server/request-context.d.ts +52 -2
  61. package/dist/types/server/rewrite-trail.d.ts +49 -0
  62. package/dist/types/server/slot-accessors.d.ts +29 -0
  63. package/dist/types/server.d.ts +1 -0
  64. package/dist/types/testing/flight.entry.d.ts +1 -1
  65. package/dist/types/testing/index.d.ts +4 -4
  66. package/dist/types/testing/internal/action-id.d.ts +10 -1
  67. package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
  68. package/dist/types/testing/internal/test-action.d.ts +8 -0
  69. package/dist/types/testing/render-route.d.ts +8 -0
  70. package/dist/types/testing/run-loader.d.ts +2 -1
  71. package/dist/types/testing/run-middleware.d.ts +20 -0
  72. package/dist/types/testing/run-revalidate.d.ts +74 -0
  73. package/dist/types/testing/run-transition-when.d.ts +2 -3
  74. package/dist/types/testing/serve-shell-request.d.ts +40 -1
  75. package/dist/types/testing/vitest.d.ts +11 -0
  76. package/dist/types/thenable-status.d.ts +6 -0
  77. package/dist/types/types/boundaries.d.ts +9 -0
  78. package/dist/types/types/handler-context.d.ts +14 -0
  79. package/dist/types/types/loader-types.d.ts +30 -0
  80. package/dist/types/types/request-scope.d.ts +7 -0
  81. package/dist/types/types/segments.d.ts +21 -4
  82. package/dist/types/urls/path-helper-types.d.ts +25 -4
  83. package/dist/types/urls/type-extraction.d.ts +6 -2
  84. package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
  85. package/dist/types/vite/discovery/state.d.ts +14 -4
  86. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
  87. package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
  88. package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
  89. package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
  90. package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
  91. package/dist/types/vite/router-discovery.d.ts +10 -0
  92. package/dist/vite/index.js +974 -379
  93. package/package.json +1 -1
  94. package/skills/caching/SKILL.md +54 -5
  95. package/skills/catalog.json +3 -3
  96. package/skills/client-urls/SKILL.md +197 -24
  97. package/skills/cloudflare/SKILL.md +14 -9
  98. package/skills/hooks/SKILL.md +16 -3
  99. package/skills/hooks/data.md +3 -1
  100. package/skills/links/SKILL.md +24 -0
  101. package/skills/loader/SKILL.md +199 -2
  102. package/skills/middleware/SKILL.md +183 -2
  103. package/skills/migrate-nextjs/SKILL.md +2 -2
  104. package/skills/migrate-react-router/SKILL.md +10 -7
  105. package/skills/migrate-react-router/data-and-actions.md +1 -1
  106. package/skills/ppr/SKILL.md +22 -0
  107. package/skills/prerender/SKILL.md +22 -8
  108. package/skills/rango/SKILL.md +13 -7
  109. package/skills/route/SKILL.md +139 -0
  110. package/skills/server-actions/SKILL.md +27 -0
  111. package/skills/testing/SKILL.md +3 -2
  112. package/skills/testing/client-components.md +2 -1
  113. package/skills/testing/middleware.md +31 -10
  114. package/skills/testing/predicates.md +67 -32
  115. package/skills/testing/response-routes.md +1 -0
  116. package/skills/testing/serve-shell-request.md +107 -19
  117. package/skills/testing/setup.md +2 -2
  118. package/skills/typesafety/route-types.md +2 -2
  119. package/skills/use-cache/SKILL.md +4 -2
  120. package/skills/view-transitions/SKILL.md +26 -0
  121. package/src/__augment-tests__/augmented.check.ts +20 -0
  122. package/src/browser/defer.ts +82 -0
  123. package/src/browser/document-revealed.ts +52 -0
  124. package/src/browser/event-controller.ts +46 -32
  125. package/src/browser/merge-segment-loaders.ts +31 -18
  126. package/src/browser/navigation-bridge.ts +82 -16
  127. package/src/browser/navigation-client.ts +48 -36
  128. package/src/browser/navigation-store.ts +13 -0
  129. package/src/browser/navigation-transaction.ts +25 -8
  130. package/src/browser/partial-update.ts +554 -146
  131. package/src/browser/prefetch/fetch.ts +2 -1
  132. package/src/browser/react/Link.tsx +7 -9
  133. package/src/browser/react/NavigationProvider.tsx +88 -49
  134. package/src/browser/react/context.ts +3 -2
  135. package/src/browser/react/filter-segment-order.ts +13 -2
  136. package/src/browser/react/use-router.ts +2 -4
  137. package/src/browser/response-adapter.ts +20 -0
  138. package/src/browser/rsc-router.tsx +17 -45
  139. package/src/browser/server-action-bridge.ts +25 -63
  140. package/src/browser/types.ts +24 -10
  141. package/src/browser/validate-redirect-origin.ts +16 -0
  142. package/src/build/generate-route-types.ts +1 -0
  143. package/src/build/route-types/router-processing.ts +3 -6
  144. package/src/build/route-types/scan-filter.ts +45 -11
  145. package/src/cache/cf/cf-cache-store.ts +59 -21
  146. package/src/cache/cf/cf-cache-types.ts +24 -2
  147. package/src/cache/document-cache-policy.ts +131 -0
  148. package/src/cache/document-cache.ts +21 -107
  149. package/src/client-urls/client-urls.ts +22 -6
  150. package/src/client-urls/hoisted-revalidate.ts +25 -0
  151. package/src/client-urls/navigation.ts +0 -96
  152. package/src/client-urls/revalidate-registry.ts +30 -0
  153. package/src/client-urls/server-projection.ts +151 -65
  154. package/src/client-urls/types.ts +39 -71
  155. package/src/client.tsx +9 -5
  156. package/src/context-var.ts +32 -2
  157. package/src/errors.ts +34 -0
  158. package/src/handles/deferred-resolution.ts +28 -5
  159. package/src/handles/pushed-by.ts +148 -0
  160. package/src/index.rsc.ts +8 -0
  161. package/src/index.ts +7 -0
  162. package/src/loader-store.ts +18 -7
  163. package/src/prefetch-markers.ts +13 -0
  164. package/src/response-utils.ts +19 -0
  165. package/src/reverse.ts +17 -14
  166. package/src/route-content-wrapper.tsx +18 -4
  167. package/src/route-definition/dsl-helpers.ts +27 -4
  168. package/src/route-definition/helpers-types.ts +27 -1
  169. package/src/route-definition/redirect.ts +2 -15
  170. package/src/route-definition/rewrite.ts +146 -0
  171. package/src/route-map-builder.ts +14 -11
  172. package/src/router/basename.ts +20 -0
  173. package/src/router/handler-context.ts +3 -13
  174. package/src/router/intercept-resolution.ts +35 -16
  175. package/src/router/internal-params.ts +16 -0
  176. package/src/router/is-action.ts +2 -1
  177. package/src/router/loader-resolution.ts +85 -4
  178. package/src/router/match-api.ts +36 -1
  179. package/src/router/match-middleware/background-revalidation.ts +16 -4
  180. package/src/router/match-middleware/cache-lookup.ts +35 -7
  181. package/src/router/match-middleware/cache-store.ts +15 -1
  182. package/src/router/match-middleware/intercept-resolution.ts +4 -2
  183. package/src/router/match-result.ts +14 -2
  184. package/src/router/middleware-types.ts +16 -0
  185. package/src/router/middleware.ts +127 -49
  186. package/src/router/navigation-snapshot.ts +2 -1
  187. package/src/router/revalidation.ts +1 -1
  188. package/src/router/route-trie-builder.ts +28 -1
  189. package/src/router/segment-resolution/fresh.ts +22 -18
  190. package/src/router/segment-resolution/loader-cache.ts +2 -1
  191. package/src/router/segment-resolution/loader-mask.ts +5 -3
  192. package/src/router/segment-resolution/loader-snapshot.ts +18 -4
  193. package/src/router/segment-resolution/mask-nested.ts +8 -4
  194. package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
  195. package/src/router/segment-resolution/revalidation.ts +215 -82
  196. package/src/rsc/full-payload.ts +37 -5
  197. package/src/rsc/handler.ts +82 -55
  198. package/src/rsc/helpers.ts +24 -0
  199. package/src/rsc/loader-fetch.ts +79 -24
  200. package/src/rsc/response-cache-serve.ts +18 -2
  201. package/src/rsc/response-route-handler.ts +4 -7
  202. package/src/rsc/rewrite-passes.ts +203 -0
  203. package/src/rsc/rsc-rendering.ts +54 -9
  204. package/src/rsc/shell-capture.ts +2 -1
  205. package/src/rsc/ssr-setup.ts +11 -5
  206. package/src/segment-loader-promise.ts +23 -20
  207. package/src/segment-system.tsx +60 -26
  208. package/src/server/context.ts +39 -5
  209. package/src/server/cookie-parse.ts +36 -12
  210. package/src/server/cookie-store.ts +7 -3
  211. package/src/server/handle-store.ts +152 -25
  212. package/src/server/request-context.ts +175 -64
  213. package/src/server/request-headers.ts +4 -14
  214. package/src/server/rewrite-trail.ts +190 -0
  215. package/src/server/slot-accessors.ts +57 -0
  216. package/src/server.ts +4 -0
  217. package/src/ssr/index.tsx +118 -16
  218. package/src/testing/action-form.ts +2 -11
  219. package/src/testing/dispatch.ts +138 -67
  220. package/src/testing/e2e/page-helpers.ts +3 -1
  221. package/src/testing/flight.entry.ts +1 -0
  222. package/src/testing/index.ts +7 -3
  223. package/src/testing/internal/action-id.ts +16 -1
  224. package/src/testing/internal/client-urls-projection.ts +70 -0
  225. package/src/testing/internal/test-action.ts +25 -0
  226. package/src/testing/render-route.tsx +16 -3
  227. package/src/testing/run-loader.ts +4 -1
  228. package/src/testing/run-middleware.ts +56 -11
  229. package/src/testing/run-revalidate.ts +142 -0
  230. package/src/testing/run-transition-when.ts +7 -8
  231. package/src/testing/serve-shell-request.ts +131 -8
  232. package/src/testing/vitest.ts +67 -1
  233. package/src/thenable-status.ts +11 -0
  234. package/src/types/boundaries.ts +12 -0
  235. package/src/types/handler-context.ts +14 -0
  236. package/src/types/loader-types.ts +30 -0
  237. package/src/types/request-scope.ts +8 -0
  238. package/src/types/segments.ts +26 -4
  239. package/src/urls/path-helper-types.ts +27 -5
  240. package/src/urls/type-extraction.ts +30 -10
  241. package/src/use-loader.tsx +18 -13
  242. package/src/vite/discovery/client-urls-projection.ts +20 -21
  243. package/src/vite/discovery/discover-routers.ts +94 -26
  244. package/src/vite/discovery/state.ts +28 -4
  245. package/src/vite/discovery/virtual-module-codegen.ts +22 -17
  246. package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
  247. package/src/vite/plugins/expose-internal-ids.ts +2 -3
  248. package/src/vite/plugins/hoist/literal.ts +417 -0
  249. package/src/vite/plugins/hoist/scope.ts +370 -0
  250. package/src/vite/plugins/hoist/source.ts +84 -0
  251. package/src/vite/plugins/transition-when-hoist.ts +113 -779
  252. package/src/vite/rango.ts +2 -0
  253. package/src/vite/router-discovery.ts +116 -53
  254. package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
  255. package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
  256. package/dist/types/testing/run-client-revalidate.d.ts +0 -43
  257. package/src/client-urls/revalidate-chain.ts +0 -83
  258. package/src/client-urls/revalidation-protocol.ts +0 -56
  259. package/src/testing/run-client-revalidate.ts +0 -92
@@ -17,6 +17,8 @@ import {
17
17
  createRequestContext,
18
18
  captureRequestEntryContext,
19
19
  } from "../server/request-context.js";
20
+ import { REWRITE_HEADER, runRewritePasses } from "./rewrite-passes.js";
21
+ import { renderedLocation } from "./full-payload.js";
20
22
  import * as rscDeps from "@vitejs/plugin-rsc/rsc/server";
21
23
  import type {
22
24
  RscPayload,
@@ -25,6 +27,7 @@ import type {
25
27
  SSRModule,
26
28
  } from "./types.js";
27
29
  import {
30
+ createReloadResponse,
28
31
  createResponseWithMergedHeaders,
29
32
  finalizeResponse,
30
33
  interceptRedirectForPartial,
@@ -41,6 +44,7 @@ import { resolvedHandleStream } from "../handles/deferred-resolution.js";
41
44
  import {
42
45
  isWebSocketUpgradeResponse,
43
46
  appendVaryAccept,
47
+ trySetHeader,
44
48
  } from "../response-utils.js";
45
49
  import {
46
50
  handleResponseRoute,
@@ -57,10 +61,7 @@ import {
57
61
  invokeOnError,
58
62
  resolveDefaultNotFound,
59
63
  } from "../router/error-handling.js";
60
- import {
61
- createReverseFunction,
62
- stripInternalParams,
63
- } from "../router/handler-context.js";
64
+ import { createReverseFunction } from "../router/handler-context.js";
64
65
  import { contextSet } from "../context-var.js";
65
66
  import {
66
67
  hasCachedManifest,
@@ -82,7 +83,7 @@ import {
82
83
  revalidateAfterAction,
83
84
  type ActionContinuation,
84
85
  } from "./server-action.js";
85
- import { handleLoaderFetch } from "./loader-fetch.js";
86
+ import { handleLoaderFetch, renderLoaderError } from "./loader-fetch.js";
86
87
  import { applyStreamIdleTimeout } from "./stream-idle.js";
87
88
  import {
88
89
  checkRequestOrigin,
@@ -113,6 +114,7 @@ import {
113
114
  mayNeedSSR,
114
115
  isRscRequest,
115
116
  SSR_SETUP_VAR,
117
+ type SSRSetup,
116
118
  } from "./ssr-setup.js";
117
119
  import {
118
120
  classifyRequest,
@@ -155,21 +157,6 @@ import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
155
157
  * ```
156
158
  */
157
159
 
158
- /**
159
- * Response that tells the client to do a full document navigation. Shared by
160
- * the terminal reload plans (version-mismatch and app-switch): an empty 200
161
- * carrying X-RSC-Reload, which the client turns into window.location.href.
162
- */
163
- function createReloadResponse(reloadUrl: string) {
164
- return createResponseWithMergedHeaders(null, {
165
- status: 200,
166
- headers: {
167
- "X-RSC-Reload": reloadUrl,
168
- "content-type": "text/x-component;charset=utf-8",
169
- },
170
- });
171
- }
172
-
173
160
  export function createRSCHandler<
174
161
  TEnv = unknown,
175
162
  TRoutes extends Record<string, string> = Record<string, string>,
@@ -703,24 +690,70 @@ export function createRSCHandler<
703
690
  // latter is reserved for the low-cardinality template (OTel convention).
704
691
  span.setAttribute("url.path", url.pathname);
705
692
 
706
- // Core handler logic (wrapped by middleware)
707
- const coreHandler = async (): Promise<Response> => {
708
- return coreRequestHandler(request, env, url, variables, nonce);
709
- };
710
-
711
- // Execute middleware chain if any, otherwise call core handler
712
- // directly; the response is finalized below, inside the response span.
713
- const hasMiddleware = matchedMiddleware.length > 0;
714
- const downstream = hasMiddleware
715
- ? await executeMiddleware(
716
- matchedMiddleware,
717
- request,
693
+ // One pass per URL: the global middleware chain matched on the pass's
694
+ // path, wrapping the core handler. A `throw rewrite()` from global or
695
+ // route middleware unwinds the pass, and the next one runs the same
696
+ // request as the target from the top, as a direct visit would
697
+ // (rsc/rewrite-passes.ts). The response is finalized below, inside the
698
+ // response span.
699
+ let passIndex = 0;
700
+ let hasMiddleware = false;
701
+ const { response: downstream, rewrittenTo } = await runRewritePasses({
702
+ reqCtx: requestContext,
703
+ request,
704
+ url,
705
+ routeMap: getRequiredRouteMap,
706
+ basename: router.basename,
707
+ runPass: (passRequest, passUrl) => {
708
+ const passMiddleware =
709
+ passIndex++ === 0
710
+ ? matchedMiddleware
711
+ : matchMiddleware(
712
+ requestContext.url.pathname,
713
+ router.middleware,
714
+ );
715
+ hasMiddleware = passMiddleware.length > 0;
716
+ const coreHandler = (): Promise<Response> =>
717
+ coreRequestHandler(passRequest, env, passUrl, variables, nonce);
718
+ return hasMiddleware
719
+ ? executeMiddleware(
720
+ passMiddleware,
721
+ passRequest,
722
+ env,
723
+ variables,
724
+ coreHandler,
725
+ createReverseFunction(getRequiredRouteMap()),
726
+ )
727
+ : coreHandler();
728
+ },
729
+ onHop: (pass) => {
730
+ span.setAttribute("rango.rewrite.target", pass.target.pathname);
731
+ span.setAttribute("rango.rewrite.hops", pass.hops);
732
+ },
733
+ report: (error, at) => {
734
+ callOnError(error, "routing", {
735
+ request: at.request,
736
+ url: at.url,
718
737
  env,
719
- variables,
720
- coreHandler,
721
- createReverseFunction(getRequiredRouteMap()),
722
- )
723
- : await coreHandler();
738
+ handledByBoundary: false,
739
+ });
740
+ },
741
+ // A loader fetch answers with the error `load()` rejects with.
742
+ refuse: (error) =>
743
+ url.searchParams.has("_rsc_loader")
744
+ ? renderLoaderError(
745
+ handlerCtx,
746
+ { message: (error as Error).message, name: "Error" },
747
+ 500,
748
+ {
749
+ request,
750
+ url,
751
+ env,
752
+ loaderId: url.searchParams.get("_rsc_loader") ?? "",
753
+ },
754
+ )
755
+ : undefined,
756
+ });
724
757
 
725
758
  // Final response construction + host handoff, wrapped in rango.response
726
759
  // — the explicit stream-handoff marker. The callback is synchronous, so
@@ -787,14 +820,9 @@ export function createRSCHandler<
787
820
  }
788
821
 
789
822
  const fullTiming = timingParts.join(", ");
790
- if (fullTiming && !isWebSocketUpgradeResponse(response)) {
791
- try {
792
- response.headers.set("Server-Timing", fullTiming);
793
- } catch {
794
- // Immutable headers (e.g. a passed-through platform Response) — drop
795
- // the timing header, never the response. Instrumentation must not
796
- // 500 a request.
797
- }
823
+ if (fullTiming) trySetHeader(response, "Server-Timing", fullTiming);
824
+ if (rewrittenTo !== undefined) {
825
+ trySetHeader(response, REWRITE_HEADER, rewrittenTo);
798
826
  }
799
827
 
800
828
  // Single open-redirect chokepoint: every response (PE, full-page,
@@ -956,18 +984,19 @@ export function createRSCHandler<
956
984
  // which returns { redirect: url }. Fall through to full-render.
957
985
  }
958
986
 
987
+ // A reload points at the visitor's URL, never a rewrite target's.
959
988
  if (plan.mode === "version-mismatch") {
960
989
  console.log(
961
990
  `[RSC] Version mismatch: client=${url.searchParams.get("_rsc_v")}, server=${version}. Forcing reload.`,
962
991
  );
963
- return createReloadResponse(plan.reloadUrl);
992
+ return createReloadResponse(getRequestContext(), plan.reloadUrl);
964
993
  }
965
994
 
966
995
  if (plan.mode === "app-switch") {
967
996
  // Cross-app SPA navigation crossed a host-router app boundary. Force a
968
997
  // real document navigation so the target app's document is re-established
969
998
  // (stylesheets, theme, warmup, prefetch-TTL). See request-classification.
970
- return createReloadResponse(plan.reloadUrl);
999
+ return createReloadResponse(getRequestContext(), plan.reloadUrl);
971
1000
  }
972
1001
 
973
1002
  // ---- 3. Origin guard (gate for action/loader/PE modes) ----
@@ -1137,6 +1166,9 @@ export function createRSCHandler<
1137
1166
  (mayNeedSSR(request, url) &&
1138
1167
  !isRscRequest(request, url, plan.mode === "partial-render"));
1139
1168
  if (plan.mode !== "loader" && willRenderHtml) {
1169
+ // A rewritten pass keeps the SSR module the earlier pass started
1170
+ // loading; the stream mode is resolved for the request it renders.
1171
+ const earlier = variables[SSR_SETUP_VAR] as Promise<SSRSetup> | undefined;
1140
1172
  const ssrSetup = startSSRSetup(
1141
1173
  handlerCtx,
1142
1174
  request,
@@ -1145,6 +1177,7 @@ export function createRSCHandler<
1145
1177
  router.debugPerformance
1146
1178
  ? () => getRequestContext()._metricsStore
1147
1179
  : undefined,
1180
+ earlier?.then(([ssrModule]) => ssrModule),
1148
1181
  );
1149
1182
  variables[SSR_SETUP_VAR] = ssrSetup;
1150
1183
 
@@ -1348,13 +1381,7 @@ export function createRSCHandler<
1348
1381
  `[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
1349
1382
  `Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
1350
1383
  );
1351
- return createResponseWithMergedHeaders(null, {
1352
- status: 200,
1353
- headers: {
1354
- "X-RSC-Reload": stripInternalParams(url).toString(),
1355
- "content-type": "text/x-component;charset=utf-8",
1356
- },
1357
- });
1384
+ return createReloadResponse(getRequestContext(), url);
1358
1385
  }
1359
1386
 
1360
1387
  if (treatAsPartial) {
@@ -1396,7 +1423,7 @@ export function createRSCHandler<
1396
1423
 
1397
1424
  const payload: RscPayload = {
1398
1425
  metadata: {
1399
- pathname: url.pathname,
1426
+ pathname: renderedLocation(getRequestContext(), url).pathname,
1400
1427
  routerId: router.id,
1401
1428
  basename: router.basename,
1402
1429
  segments: [notFoundSegment],
@@ -24,6 +24,7 @@ import {
24
24
  import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
25
25
  import { formatCacheSignalHeader } from "../router/telemetry.js";
26
26
  import type { RscPayload } from "./types.js";
27
+ import { rewriteTrail, visitorUrl } from "../server/rewrite-trail.js";
27
28
  import type { HandlerContext } from "./handler-context.js";
28
29
 
29
30
  /**
@@ -157,6 +158,29 @@ export function createResponseWithMergedHeaders(
157
158
  return drainOnResponseCallbacks(ctx, response);
158
159
  }
159
160
 
161
+ /**
162
+ * An empty 200 carrying X-RSC-Reload: the client turns it into a document
163
+ * navigation (window.location.href). It points at the visitor's URL: the
164
+ * first URL of a rewritten request (server/rewrite-trail.ts visitorUrl),
165
+ * else `url` with the internal `_rsc*` params stripped, or `url` as given
166
+ * when it is a string the caller resolved (an action's referer).
167
+ */
168
+ export function createReloadResponse(
169
+ reqCtx: RequestContext<any> | undefined,
170
+ url: URL | string,
171
+ ): Response {
172
+ const target =
173
+ rewriteTrail(reqCtx)?.[0] ??
174
+ (typeof url === "string" ? url : visitorUrl(undefined, url).href);
175
+ return createResponseWithMergedHeaders(null, {
176
+ status: 200,
177
+ headers: {
178
+ "X-RSC-Reload": target,
179
+ "content-type": "text/x-component;charset=utf-8",
180
+ },
181
+ });
182
+ }
183
+
160
184
  /**
161
185
  * Rebuild a Response returned or thrown by a response-route handler so request
162
186
  * effects merge consistently in production and the public dispatch primitive.
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { requestHeaders } from "../server/request-headers.js";
15
+ import { FORBIDDEN_KEYS } from "../context-var.js";
15
16
  import { getLoaderLazy } from "../server/loader-registry.js";
16
17
  import { isDataNotFoundError } from "../errors.js";
17
18
  import { executeLoaderMiddleware } from "../router/middleware.js";
@@ -33,18 +34,57 @@ import {
33
34
  } from "./helpers.js";
34
35
  import type { HandlerContext } from "./handler-context.js";
35
36
 
37
+ export interface LoaderErrorMeta<TEnv> {
38
+ request: Request;
39
+ url: URL;
40
+ env: TEnv;
41
+ loaderId: string;
42
+ }
43
+
44
+ /**
45
+ * A text/x-component Response carrying `{ loaderResult: null, loaderError }`,
46
+ * the payload `load()` turns into a rejection with `loaderError.message`. A
47
+ * plain-text body would reject with React's opaque "Connection closed.".
48
+ * Render failures report through onError("rendering").
49
+ */
50
+ export function renderLoaderError<TEnv>(
51
+ ctx: HandlerContext<TEnv>,
52
+ loaderError: { message: string; name: string },
53
+ status: number,
54
+ meta: LoaderErrorMeta<TEnv>,
55
+ ): Response {
56
+ const { request, url, env, loaderId } = meta;
57
+ const rscStream = ctx.renderToReadableStream(
58
+ { loaderResult: null, loaderError },
59
+ {
60
+ onError: (renderError: unknown) => {
61
+ ctx.callOnError(renderError, "rendering", {
62
+ request,
63
+ url,
64
+ env,
65
+ loaderName: loaderId,
66
+ });
67
+ },
68
+ },
69
+ );
70
+
71
+ return createResponseWithMergedHeaders(rscStream, {
72
+ status,
73
+ headers: { "content-type": "text/x-component;charset=utf-8" },
74
+ });
75
+ }
76
+
36
77
  /**
37
78
  * Build the 500 RSC error Response for a failed fetchable loader. Shared by the
38
79
  * module-load-error catch (the import itself threw) and the loader-execution
39
- * error catch — both call onError("loader"), serialize the same dev-gated error
40
- * payload via renderToReadableStream (reporting render failures through
41
- * onError("rendering")), and return a 500 text/x-component Response. The only
42
- * per-site difference is the console.error label, passed in.
80
+ * error catch — both call onError("loader") and serialize the same dev-gated
81
+ * error payload. The only per-site difference is the console.error label,
82
+ * passed in.
43
83
  */
44
84
  function buildLoaderErrorResponse<TEnv>(
45
85
  ctx: HandlerContext<TEnv>,
46
86
  error: unknown,
47
- meta: { request: Request; url: URL; env: TEnv; loaderId: string },
87
+ meta: LoaderErrorMeta<TEnv>,
48
88
  logLabel: string,
49
89
  ): Response {
50
90
  const { request, url, env, loaderId } = meta;
@@ -61,9 +101,9 @@ function buildLoaderErrorResponse<TEnv>(
61
101
  });
62
102
 
63
103
  const err = error instanceof Error ? error : new Error(String(error));
64
- const errorPayload = {
65
- loaderResult: null,
66
- loaderError: {
104
+ return renderLoaderError(
105
+ ctx,
106
+ {
67
107
  message: isDev ? err.message : "An error occurred",
68
108
  // Gate err.name to dev. In production it leaks the consumer's error class
69
109
  // name (e.g. AuthError, PrismaClientKnownRequestError) to the client; the
@@ -71,22 +111,9 @@ function buildLoaderErrorResponse<TEnv>(
71
111
  // Matches sanitizeError's dev-only name contract.
72
112
  name: isDev ? err.name : "Error",
73
113
  },
74
- };
75
- const rscStream = ctx.renderToReadableStream(errorPayload, {
76
- onError: (renderError: unknown) => {
77
- ctx.callOnError(renderError, "rendering", {
78
- request,
79
- url,
80
- env,
81
- loaderName: loaderId,
82
- });
83
- },
84
- });
85
-
86
- return createResponseWithMergedHeaders(rscStream, {
87
- status: 500,
88
- headers: { "content-type": "text/x-component;charset=utf-8" },
89
- });
114
+ 500,
115
+ meta,
116
+ );
90
117
  }
91
118
 
92
119
  export async function handleLoaderFetch<TEnv>(
@@ -183,6 +210,21 @@ export async function handleLoaderFetch<TEnv>(
183
210
  }
184
211
  }
185
212
 
213
+ // The params become ctx.params, so they hold what a route param can: string
214
+ // keys and string values, never a reserved key (#1109). The message is the
215
+ // router's own, so production sends it too.
216
+ if (!isLoaderParams(loaderParams)) {
217
+ return renderLoaderError(
218
+ ctx,
219
+ {
220
+ message: `Invalid loader params: expected an object of string values without the reserved keys ${[...FORBIDDEN_KEYS].join(", ")}`,
221
+ name: "Error",
222
+ },
223
+ 400,
224
+ { request, url, env, loaderId },
225
+ );
226
+ }
227
+
186
228
  // Execute the loader with middleware.
187
229
  // finalizeResponse drains onResponse callbacks that middleware short-circuits
188
230
  // may leave behind (executeLoaderMiddleware does not finalize them itself).
@@ -304,3 +346,16 @@ export async function handleLoaderFetch<TEnv>(
304
346
  );
305
347
  }
306
348
  }
349
+
350
+ function isLoaderParams(value: unknown): value is Record<string, string> {
351
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
352
+ return false;
353
+ }
354
+ for (const key of Object.keys(value)) {
355
+ if (FORBIDDEN_KEYS.has(key)) return false;
356
+ if (typeof (value as Record<string, unknown>)[key] !== "string") {
357
+ return false;
358
+ }
359
+ }
360
+ return true;
361
+ }
@@ -19,6 +19,7 @@
19
19
  * type-only import (erased at build).
20
20
  */
21
21
 
22
+ import { isRewrite } from "../route-definition/rewrite.js";
22
23
  import type { CacheScope } from "../cache/cache-scope.js";
23
24
  import {
24
25
  _getRequestContext,
@@ -159,10 +160,13 @@ export async function serveResponseRouteWithCache(
159
160
  // Finding #3 as document-cache.ts shouldCacheResponse.
160
161
  // Only store GET: HEAD may branch on method; sharing a write slot with GET
161
162
  // can poison subsequent GET bodies. HEAD still reads the GET key below.
163
+ // Nor a body a rewritten request built from the visitor's URL
164
+ // (ctx.rewrittenFrom, server/rewrite-trail.ts): the key is the target's.
162
165
  const canStore = (response: Response): boolean =>
163
166
  method === "GET" &&
164
167
  isCacheableStatus(response.status) &&
165
- !hasPerClientSignal(response.headers);
168
+ !hasPerClientSignal(response.headers) &&
169
+ !reqCtx._payloadVisitorLocation;
166
170
 
167
171
  // Reject unsafe persisted entries on read (pre-upgrade poison / store bugs).
168
172
  const canServeCached = (response: Response): boolean =>
@@ -206,6 +210,8 @@ export async function serveResponseRouteWithCache(
206
210
  const fresh = finalizeResponse(await executeHandler());
207
211
  if (canStore(fresh)) await putFresh(store, fresh, start);
208
212
  } catch (error) {
213
+ // The route now rewrites: nothing to refresh this entry with.
214
+ if (isRewrite(error)) return;
209
215
  reportCacheError(
210
216
  error,
211
217
  "stale-revalidation",
@@ -227,7 +233,17 @@ export async function serveResponseRouteWithCache(
227
233
 
228
234
  // Cache miss: execute the handler and cache the result when storeable.
229
235
  const start = executionStart();
230
- const response = finalizeResponse(await executeHandler());
236
+ let response: Response;
237
+ try {
238
+ response = finalizeResponse(await executeHandler());
239
+ } catch (error) {
240
+ // Route middleware rewrote: the request runs again as the target, whose
241
+ // response runs these pre-handler callbacks.
242
+ if (isRewrite(error)) {
243
+ reqCtx._onResponseCallbacks.unshift(...savedCallbacks);
244
+ }
245
+ throw error;
246
+ }
231
247
  if (canStore(response)) {
232
248
  // Clone SYNCHRONOUSLY here, before returning. The original `response` is
233
249
  // handed back to the middleware chain, where mergeResponse rebuilds it as
@@ -10,6 +10,7 @@ import { RouterError } from "../errors.js";
10
10
  import { getRequestContext } from "../server/request-context.js";
11
11
  import { contextGet, isNonCacheable } from "../context-var.js";
12
12
  import { NOCACHE_SYMBOL } from "../cache/taint.js";
13
+ import { defineRewrittenFrom } from "../server/rewrite-trail.js";
13
14
  import { RESPONSE_TYPE_MIME } from "../router/content-negotiation.js";
14
15
  import { createCacheScope } from "../cache/cache-scope.js";
15
16
  import { serveResponseRouteWithCache } from "./response-cache-serve.js";
@@ -26,6 +27,7 @@ import {
26
27
  import type { HandlerContext } from "./handler-context.js";
27
28
  import { createProblemDetails } from "./response-error.js";
28
29
  import {
30
+ createReloadResponse,
29
31
  createResponseWithMergedHeaders,
30
32
  finalizeResponse,
31
33
  isCacheableStatus,
@@ -67,13 +69,7 @@ export async function handleResponseRoute<TEnv>(
67
69
  // Partial requests (client-side navigation) to response routes
68
70
  // get X-RSC-Reload to trigger hard navigation in the browser
69
71
  if (isPartial) {
70
- return createResponseWithMergedHeaders(null, {
71
- status: 200,
72
- headers: {
73
- "X-RSC-Reload": stripInternalParams(url).toString(),
74
- "content-type": "text/x-component;charset=utf-8",
75
- },
76
- });
72
+ return createReloadResponse(getRequestContext(), url);
77
73
  }
78
74
 
79
75
  // Build lightweight context for response handler
@@ -101,6 +97,7 @@ export async function handleResponseRoute<TEnv>(
101
97
  };
102
98
  // Brand with taint symbol so "use cache" detects it as request-scoped
103
99
  // and extracts route-identifying properties (params, pathname, _responseType)
100
+ defineRewrittenFrom(responseHandlerCtx, reqCtx);
104
101
  (responseHandlerCtx as any)[NOCACHE_SYMBOL] = true;
105
102
 
106
103
  // Call handler directly, wrapped by route middleware if present