@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
@@ -1,37 +1,104 @@
1
1
  /**
2
- * Response Error Payload Builder
2
+ * Problem Details (RFC 9457) Builder
3
3
  *
4
- * Builds a ResponseError object from a caught error, controlling
5
- * what information is exposed based on error type and environment.
4
+ * Builds a problem+json error body from a caught error, controlling what
5
+ * information is exposed based on error type and environment.
6
6
  */
7
7
 
8
8
  import { RouterError } from "../errors.js";
9
- import type { ResponseError } from "../urls.js";
9
+ import type { ProblemDetails } from "../urls.js";
10
10
 
11
11
  /**
12
- * Build a ResponseError payload from a caught error.
13
- * RouterError messages are always exposed (developer-crafted).
12
+ * HTTP reason phrases for the problem `title` member. Inlined because the
13
+ * router targets edge/worker runtimes without node's `http.STATUS_CODES`;
14
+ * covers the full standard 4xx/5xx range, with a generic fallback for any
15
+ * non-standard status a handler might set.
16
+ */
17
+ const STATUS_PHRASES: Record<number, string> = {
18
+ 400: "Bad Request",
19
+ 401: "Unauthorized",
20
+ 402: "Payment Required",
21
+ 403: "Forbidden",
22
+ 404: "Not Found",
23
+ 405: "Method Not Allowed",
24
+ 406: "Not Acceptable",
25
+ 407: "Proxy Authentication Required",
26
+ 408: "Request Timeout",
27
+ 409: "Conflict",
28
+ 410: "Gone",
29
+ 411: "Length Required",
30
+ 412: "Precondition Failed",
31
+ 413: "Payload Too Large",
32
+ 414: "URI Too Long",
33
+ 415: "Unsupported Media Type",
34
+ 416: "Range Not Satisfiable",
35
+ 417: "Expectation Failed",
36
+ 418: "I'm a Teapot",
37
+ 421: "Misdirected Request",
38
+ 422: "Unprocessable Entity",
39
+ 423: "Locked",
40
+ 424: "Failed Dependency",
41
+ 425: "Too Early",
42
+ 426: "Upgrade Required",
43
+ 428: "Precondition Required",
44
+ 429: "Too Many Requests",
45
+ 431: "Request Header Fields Too Large",
46
+ 451: "Unavailable For Legal Reasons",
47
+ 500: "Internal Server Error",
48
+ 501: "Not Implemented",
49
+ 502: "Bad Gateway",
50
+ 503: "Service Unavailable",
51
+ 504: "Gateway Timeout",
52
+ 505: "HTTP Version Not Supported",
53
+ 506: "Variant Also Negotiates",
54
+ 507: "Insufficient Storage",
55
+ 508: "Loop Detected",
56
+ 510: "Not Extended",
57
+ 511: "Network Authentication Required",
58
+ };
59
+
60
+ function statusPhrase(status: number): string {
61
+ return STATUS_PHRASES[status] ?? "Error";
62
+ }
63
+
64
+ /**
65
+ * Build an RFC 9457 problem+json body from a caught error.
66
+ * RouterError messages/codes are always exposed (developer-crafted).
14
67
  * Standard Error messages are hidden in production.
68
+ *
69
+ * The `type` member is omitted in this phase: per RFC 9457 an absent `type` is
70
+ * treated as `"about:blank"` (no semantics beyond the HTTP status), so emitting
71
+ * it adds nothing. Per-route problem-type URIs arrive with the declared-errors
72
+ * map later. `code` is always present so consumers can branch on it
73
+ * (`"INTERNAL"` for non-RouterError failures).
15
74
  */
16
- export function createResponseErrorPayload(
75
+ export function createProblemDetails(
17
76
  error: unknown,
77
+ status: number,
18
78
  isDev: boolean,
19
- ): ResponseError {
79
+ ): ProblemDetails {
20
80
  if (error instanceof RouterError) {
21
81
  return {
22
- message: error.message,
82
+ title: statusPhrase(status),
83
+ status,
84
+ detail: error.message,
23
85
  code: error.code,
24
- ...(error.type ? { type: error.type } : {}),
25
86
  ...(isDev && error.stack ? { stack: error.stack } : {}),
26
87
  };
27
88
  }
28
89
  if (error instanceof Error) {
29
90
  return {
30
- message: isDev ? error.message : "Internal Server Error",
91
+ title: statusPhrase(status),
92
+ status,
93
+ detail: isDev ? error.message : "Internal Server Error",
94
+ code: "INTERNAL",
31
95
  ...(isDev && error.stack ? { stack: error.stack } : {}),
32
96
  };
33
97
  }
34
98
  return {
35
- message: isDev ? String(error) : "Internal Server Error",
99
+ title: statusPhrase(status),
100
+ status,
101
+ detail: isDev ? String(error) : "Internal Server Error",
102
+ code: "INTERNAL",
36
103
  };
37
104
  }
@@ -7,11 +7,12 @@
7
7
  */
8
8
 
9
9
  import { RouterError } from "../errors.js";
10
- import { requireRequestContext } from "../server/request-context.js";
10
+ import { getRequestContext } from "../server/request-context.js";
11
11
  import { contextGet } from "../context-var.js";
12
12
  import { NOCACHE_SYMBOL } from "../cache/taint.js";
13
- import { traverseBack } from "../router/pattern-matching.js";
14
- import { createCacheScope } from "../cache/cache-scope.js";
13
+ import { RESPONSE_TYPE_MIME } from "../router/content-negotiation.js";
14
+ import { createCacheScope, resolveCacheTags } from "../cache/cache-scope.js";
15
+ import { serveResponseRouteWithCache } from "./response-cache-serve.js";
15
16
  import { executeMiddleware } from "../router/middleware.js";
16
17
  import {
17
18
  createReverseFunction,
@@ -20,7 +21,7 @@ import {
20
21
  import type { MiddlewareFn } from "../router/middleware.js";
21
22
  import type { EntryData } from "../server/context.js";
22
23
  import type { HandlerContext } from "./handler-context.js";
23
- import { createResponseErrorPayload } from "./response-error.js";
24
+ import { createProblemDetails } from "./response-error.js";
24
25
  import {
25
26
  createResponseWithMergedHeaders,
26
27
  finalizeResponse,
@@ -29,6 +30,12 @@ import {
29
30
  mergeStubHeadersAndFinalize,
30
31
  } from "./helpers.js";
31
32
  import { isWebSocketUpgradeResponse } from "../response-utils.js";
33
+ import { stringifyJsonRouteResult } from "./json-route-result.js";
34
+ import {
35
+ EXTERNAL_REDIRECT_MARKER,
36
+ isExternalRedirect,
37
+ markExternalRedirect,
38
+ } from "../redirect-origin.js";
32
39
 
33
40
  export interface ResponseRouteMatch {
34
41
  responseType: string;
@@ -72,7 +79,7 @@ export async function handleResponseRoute<TEnv>(
72
79
  }
73
80
 
74
81
  // Build lightweight context for response handler
75
- const reqCtx = requireRequestContext();
82
+ const reqCtx = getRequestContext();
76
83
  const cleanUrl = stripInternalParams(url);
77
84
  const responseHandlerCtx = {
78
85
  request,
@@ -80,10 +87,13 @@ export async function handleResponseRoute<TEnv>(
80
87
  env,
81
88
  searchParams: cleanUrl.searchParams,
82
89
  url: cleanUrl,
90
+ originalUrl: reqCtx.originalUrl,
83
91
  pathname: url.pathname,
84
92
  reverse: createReverseFunction(handlerCtx.getRequiredRouteMap()),
85
93
  get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
86
94
  header: (name: string, value: string) => reqCtx.header(name, value),
95
+ waitUntil: reqCtx.waitUntil.bind(reqCtx),
96
+ executionContext: reqCtx.executionContext,
87
97
  _responseType: preview.responseType,
88
98
  };
89
99
  // Brand with taint symbol so "use cache" detects it as request-scoped
@@ -106,49 +116,31 @@ export async function handleResponseRoute<TEnv>(
106
116
  }
107
117
  const headers = new Headers();
108
118
  result.headers.forEach((value, key) => {
119
+ // Never copy the reserved external-redirect marker off a handler result.
120
+ // It is not a trust signal -- the opt-in is the out-of-band brand below
121
+ // -- and a proxy-style route returning an attacker-controlled upstream
122
+ // response must not let a forged value ride through to the browser.
123
+ if (key.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
109
124
  if (key.toLowerCase() === "set-cookie") {
110
125
  headers.append(key, value);
111
126
  } else {
112
127
  headers.set(key, value);
113
128
  }
114
129
  });
115
- return createResponseWithMergedHeaders(result.body, {
130
+ const rewrapped = createResponseWithMergedHeaders(result.body, {
116
131
  status: result.status,
117
132
  headers,
118
133
  });
119
- };
120
-
121
- // JSON response routes: wrap in { data } / { error } envelope
122
- if (preview.responseType === "json") {
123
- try {
124
- const result = await (preview.handler as Function)(responseHandlerCtx);
125
- if (result instanceof Response) {
126
- return rewrapResponse(result);
127
- }
128
- return createResponseWithMergedHeaders(
129
- JSON.stringify({ data: result }),
130
- {
131
- status: 200,
132
- headers: { "content-type": "application/json;charset=utf-8" },
133
- },
134
- );
135
- } catch (error) {
136
- handlerCtx.callOnError(error, "handler", errorCtx);
137
- const isDev = process.env.NODE_ENV !== "production";
138
- const status = error instanceof RouterError ? error.status : 500;
139
- return createResponseWithMergedHeaders(
140
- JSON.stringify({
141
- error: createResponseErrorPayload(error, isDev),
142
- }),
143
- {
144
- status,
145
- headers: { "content-type": "application/json;charset=utf-8" },
146
- },
147
- );
134
+ // Transfer the out-of-band external brand only when the handler result is
135
+ // genuinely branded (a real redirect(url, { external: true })). A proxied
136
+ // upstream Response is never branded, so an attacker cannot opt a response
137
+ // route's redirect out of the same-origin guard by injecting the header.
138
+ if (isExternalRedirect(result)) {
139
+ markExternalRedirect(rewrapped);
148
140
  }
149
- }
141
+ return rewrapped;
142
+ };
150
143
 
151
- // Non-JSON response routes: catch errors and return plain Response
152
144
  try {
153
145
  const result = await (preview.handler as Function)(responseHandlerCtx);
154
146
 
@@ -156,38 +148,56 @@ export async function handleResponseRoute<TEnv>(
156
148
  return rewrapResponse(result);
157
149
  }
158
150
 
159
- // Auto-wrap based on response type tag
160
- switch (preview.responseType) {
161
- case "text":
162
- return createResponseWithMergedHeaders(String(result), {
163
- status: 200,
164
- headers: { "content-type": "text/plain;charset=utf-8" },
165
- });
166
- case "html":
167
- return createResponseWithMergedHeaders(String(result), {
168
- status: 200,
169
- headers: { "content-type": "text/html;charset=utf-8" },
170
- });
171
- case "xml":
172
- return createResponseWithMergedHeaders(String(result), {
173
- status: 200,
174
- headers: { "content-type": "application/xml;charset=utf-8" },
175
- });
176
- case "md":
177
- return createResponseWithMergedHeaders(String(result), {
178
- status: 200,
179
- headers: { "content-type": "text/markdown;charset=utf-8" },
180
- });
181
- default:
182
- // image, stream, any -- must return Response
183
- throw new Error(
184
- `Response route handler for "${preview.responseType}" must return a Response object, got ${typeof result}`,
185
- );
151
+ // Handled before the MIME lookup (json is also a RESPONSE_TYPE_MIME key).
152
+ if (preview.responseType === "json") {
153
+ // Runtime guard: the json() return type rejects nested Promises at
154
+ // compile time, but an `as`-cast or untyped (JS) handler can still slip
155
+ // one through. stringifyJsonRouteResult throws a clear error instead of
156
+ // shipping empty data (shared with dispatch() so the two cannot drift).
157
+ const body = stringifyJsonRouteResult(result);
158
+ return createResponseWithMergedHeaders(body, {
159
+ status: 200,
160
+ headers: { "content-type": "application/json;charset=utf-8" },
161
+ });
162
+ }
163
+
164
+ // Object.hasOwn (not truthiness) so prototype names like "toString" are not
165
+ // matched; image/stream/any are absent and fall through to the throw.
166
+ if (Object.hasOwn(RESPONSE_TYPE_MIME, preview.responseType)) {
167
+ return createResponseWithMergedHeaders(String(result), {
168
+ status: 200,
169
+ headers: {
170
+ "content-type": `${RESPONSE_TYPE_MIME[preview.responseType]};charset=utf-8`,
171
+ },
172
+ });
186
173
  }
174
+
175
+ throw new Error(
176
+ `Response route handler for "${preview.responseType}" must return a Response object, got ${typeof result}`,
177
+ );
187
178
  } catch (error) {
188
179
  handlerCtx.callOnError(error, "handler", errorCtx);
189
180
  const isDev = process.env.NODE_ENV !== "production";
190
- const status = error instanceof RouterError ? error.status : 500;
181
+ const derivedStatus = error instanceof RouterError ? error.status : 500;
182
+ // Resolve the effective status the same way createResponseWithMergedHeaders
183
+ // will (ctx.res.status override) so the problem body's status/title match
184
+ // the actual HTTP status — e.g. when a handler called ctx.setStatus()
185
+ // before throwing.
186
+ const status =
187
+ reqCtx.res.status !== 200 ? reqCtx.res.status : derivedStatus;
188
+
189
+ if (preview.responseType === "json") {
190
+ return createResponseWithMergedHeaders(
191
+ JSON.stringify(createProblemDetails(error, status, isDev)),
192
+ {
193
+ status,
194
+ headers: {
195
+ "content-type": "application/problem+json;charset=utf-8",
196
+ },
197
+ },
198
+ );
199
+ }
200
+
191
201
  const message =
192
202
  error instanceof RouterError
193
203
  ? error.message
@@ -227,130 +237,20 @@ export async function handleResponseRoute<TEnv>(
227
237
  return callHandlerWithVary();
228
238
  };
229
239
 
230
- // Resolve cache config from entry tree (same pattern as match-api.ts)
240
+ // Response-route cache: resolved through the shared serve leaf
241
+ // (rsc/response-cache-serve.ts) so production and the dispatch testing
242
+ // primitive share ONE owner of the cache contract. Returns undefined when no
243
+ // cache applies, so we fall through to a plain handler run.
231
244
  if (preview.manifestEntry) {
232
- let cacheScope: ReturnType<typeof createCacheScope> = null;
233
- for (const entry of traverseBack(preview.manifestEntry)) {
234
- if (entry.cache) {
235
- cacheScope = createCacheScope(entry.cache, cacheScope);
236
- }
237
- }
238
-
239
- if (cacheScope?.enabled) {
240
- // Evaluate condition skip response cache when condition returns false
241
- let conditionPassed = true;
242
- if (cacheScope.config !== false && cacheScope.config.condition) {
243
- try {
244
- conditionPassed = !!cacheScope.config.condition(reqCtx);
245
- } catch {
246
- conditionPassed = false;
247
- }
248
- }
249
-
250
- const store = cacheScope.getStore() ?? reqCtx._cacheStore;
251
- if (conditionPassed && store?.getResponse && store?.putResponse) {
252
- // Build cache key with response:{type}: prefix to avoid collision
253
- // with segment keys and differentiate between response types.
254
- // Include host and url.search so query-driven and multi-host
255
- // responses cache separately.
256
- let cacheKey = `response:${preview.responseType}:${url.host}${url.pathname}${url.search}`;
257
-
258
- // Priority 1: Route-level key function (full override)
259
- if (cacheScope.config !== false && cacheScope.config.key) {
260
- try {
261
- const customKey = await cacheScope.config.key(reqCtx);
262
- cacheKey = `response:${customKey}`;
263
- } catch {
264
- // Fall back to default key on route-level key failure
265
- }
266
- } else if (store.keyGenerator) {
267
- // Priority 2: Store-level keyGenerator (modifies default key)
268
- try {
269
- cacheKey = await store.keyGenerator(reqCtx, cacheKey);
270
- } catch {
271
- // Fall back to default key on keyGenerator failure
272
- }
273
- }
274
-
275
- // Save pre-handler callbacks (registered by app-level middleware
276
- // before we reach the cache block) and clear the live array.
277
- // createResponseWithMergedHeaders (inside the handler) eagerly
278
- // executes any callbacks present in _onResponseCallbacks, so
279
- // handler-registered callbacks are baked into the handler's
280
- // response and the cached artifact. Pre-handler callbacks are
281
- // NOT in the live array during execution, so they are applied
282
- // once per serve on every path (hit + miss) below.
283
- const savedCallbacks = reqCtx._onResponseCallbacks;
284
- reqCtx._onResponseCallbacks = [];
285
-
286
- const applyPreHandlerCallbacks = (response: Response): Response => {
287
- let result = response;
288
- for (const callback of savedCallbacks) {
289
- result = callback(result) ?? result;
290
- }
291
- return result;
292
- };
293
-
294
- try {
295
- const cached = await store.getResponse(cacheKey);
296
-
297
- if (cached && isCacheableStatus(cached.response.status)) {
298
- if (!cached.shouldRevalidate) {
299
- // Fresh hit
300
- return applyPreHandlerCallbacks(cached.response);
301
- }
302
-
303
- // Stale hit (SWR) - return cached, revalidate in background
304
- reqCtx.waitUntil(async () => {
305
- try {
306
- // finalizeResponse drains any onResponse callbacks registered
307
- // during middleware execution (e.g. middleware short-circuit)
308
- // that createResponseWithMergedHeaders didn't reach.
309
- const fresh = finalizeResponse(await executeHandler());
310
- if (isCacheableStatus(fresh.status)) {
311
- await store.putResponse!(
312
- cacheKey,
313
- fresh.clone(),
314
- cacheScope!.ttl,
315
- cacheScope!.swr,
316
- );
317
- }
318
- } catch (error) {
319
- console.error(`[ResponseCache] Revalidation failed:`, error);
320
- }
321
- });
322
-
323
- return applyPreHandlerCallbacks(cached.response);
324
- }
325
- } catch (error) {
326
- console.error(`[ResponseCache] Cache lookup failed:`, error);
327
- }
328
-
329
- // Cache miss - execute handler and cache the result.
330
- // createResponseWithMergedHeaders inside the handler drains callbacks
331
- // registered during handler execution. finalizeResponse catches any
332
- // remaining callbacks (e.g. from middleware short-circuit where the
333
- // handler never ran) so the cached artifact includes all transforms.
334
- const response = finalizeResponse(await executeHandler());
335
-
336
- if (isCacheableStatus(response.status)) {
337
- reqCtx.waitUntil(async () => {
338
- try {
339
- await store.putResponse!(
340
- cacheKey,
341
- response.clone(),
342
- cacheScope!.ttl,
343
- cacheScope!.swr,
344
- );
345
- } catch (error) {
346
- console.error(`[ResponseCache] Cache write failed:`, error);
347
- }
348
- });
349
- }
350
-
351
- return applyPreHandlerCallbacks(response);
352
- }
353
- }
245
+ const cached = await serveResponseRouteWithCache({
246
+ reqCtx,
247
+ manifestEntry: preview.manifestEntry,
248
+ responseType: preview.responseType,
249
+ url,
250
+ executeHandler,
251
+ deps: { createCacheScope, resolveCacheTags },
252
+ });
253
+ if (cached !== undefined) return cached;
354
254
  }
355
255
 
356
256
  return executeHandler().then(finalizeResponse);