@rangojs/router 0.0.0-experimental.98 → 0.0.0-experimental.98914650
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.
- package/README.md +24 -9
- package/dist/bin/rango.js +157 -63
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +1584 -639
- package/package.json +60 -11
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +222 -30
- package/skills/caching/SKILL.md +263 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +235 -28
- package/skills/host-router/SKILL.md +122 -22
- package/skills/intercept/SKILL.md +29 -5
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +170 -23
- package/skills/middleware/SKILL.md +16 -10
- package/skills/migrate-nextjs/SKILL.md +38 -16
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +11 -7
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +250 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +114 -47
- package/skills/route/SKILL.md +22 -5
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +78 -42
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +310 -26
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/vercel/SKILL.md +107 -0
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +37 -143
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +30 -59
- package/src/browser/navigation-client.ts +96 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +32 -82
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/partial-update.ts +60 -127
- package/src/browser/prefetch/cache.ts +82 -72
- package/src/browser/prefetch/fetch.ts +108 -33
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +41 -48
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +3 -6
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +20 -5
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +70 -34
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +168 -44
- package/src/browser/types.ts +36 -21
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +89 -11
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +122 -22
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +134 -32
- package/src/cache/cache-scope.ts +100 -74
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2255 -238
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +61 -20
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +22 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +25 -61
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +17 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +63 -9
- package/src/index.ts +64 -9
- package/src/internal-debug.ts +2 -4
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -41
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +244 -281
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +40 -17
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -15
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +4 -42
- package/src/router/intercept-resolution.ts +14 -19
- package/src/router/lazy-includes.ts +9 -46
- package/src/router/loader-resolution.ts +91 -46
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +150 -271
- package/src/router/match-middleware/cache-store.ts +3 -33
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +31 -80
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +118 -133
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +20 -58
- package/src/router/prerender-match.ts +99 -63
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +50 -56
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +68 -35
- package/src/router/router-options.ts +55 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +44 -63
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +40 -37
- package/src/router/segment-resolution/revalidation.ts +203 -285
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +87 -47
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +80 -41
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +83 -78
- package/src/rsc/helpers.ts +93 -5
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +12 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +76 -62
- package/src/rsc/rsc-rendering.ts +41 -60
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +62 -67
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +10 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +199 -142
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +150 -51
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +165 -87
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +97 -22
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +18 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +19 -5
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +95 -82
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +26 -34
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +39 -1
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +185 -10
- package/src/vite/plugins/cjs-to-esm.ts +3 -18
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +12 -11
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +4 -75
- package/src/vite/plugins/expose-id-utils.ts +3 -54
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +57 -67
- package/src/vite/plugins/performance-tracks.ts +9 -16
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +26 -49
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +2 -32
- package/src/vite/plugins/version-plugin.ts +32 -23
- package/src/vite/plugins/virtual-entries.ts +35 -17
- package/src/vite/rango.ts +148 -115
- package/src/vite/router-discovery.ts +220 -68
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -35
- package/src/vite/utils/shared-utils.ts +95 -43
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
package/src/rsc/origin-guard.ts
CHANGED
|
@@ -9,11 +9,31 @@
|
|
|
9
9
|
* navigations, bookmarks, and non-browser clients don't send Origin.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import type { RequestPlan } from "../router/request-classification.js";
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Request phase that triggered the origin check.
|
|
14
16
|
*/
|
|
15
17
|
export type OriginCheckPhase = "action" | "loader" | "pe-form";
|
|
16
18
|
|
|
19
|
+
// Exhaustive over RequestPlan modes so a new mode must be classified here (the
|
|
20
|
+
// security gate) instead of silently falling through to no origin check.
|
|
21
|
+
export const ORIGIN_CHECK_PHASE_BY_MODE: Record<
|
|
22
|
+
RequestPlan["mode"],
|
|
23
|
+
OriginCheckPhase | null
|
|
24
|
+
> = {
|
|
25
|
+
action: "action",
|
|
26
|
+
loader: "loader",
|
|
27
|
+
"pe-render": "pe-form",
|
|
28
|
+
"full-render": null,
|
|
29
|
+
"partial-render": null,
|
|
30
|
+
response: null,
|
|
31
|
+
redirect: null,
|
|
32
|
+
"version-mismatch": null,
|
|
33
|
+
// Terminal: handled before the origin guard (emits X-RSC-Reload, no execution).
|
|
34
|
+
"app-switch": null,
|
|
35
|
+
};
|
|
36
|
+
|
|
17
37
|
/**
|
|
18
38
|
* Context passed to the originCheck callback.
|
|
19
39
|
*/
|
|
@@ -49,11 +69,8 @@ export type OriginCheckConfig<TEnv = any> =
|
|
|
49
69
|
* Returns true to allow, false to reject.
|
|
50
70
|
*/
|
|
51
71
|
export function defaultOriginCheck(request: Request, url: URL): boolean {
|
|
52
|
-
// 1. Read Origin header (present on all cross-origin requests and
|
|
53
|
-
// same-origin POST/PUT/PATCH/DELETE in modern browsers)
|
|
54
72
|
let requestOrigin = request.headers.get("origin");
|
|
55
73
|
|
|
56
|
-
// 2. Fallback to Referer if Origin is absent (some proxies strip it)
|
|
57
74
|
if (!requestOrigin) {
|
|
58
75
|
const referer = request.headers.get("referer");
|
|
59
76
|
if (referer) {
|
|
@@ -65,23 +82,20 @@ export function defaultOriginCheck(request: Request, url: URL): boolean {
|
|
|
65
82
|
}
|
|
66
83
|
}
|
|
67
84
|
|
|
68
|
-
// 3. No Origin or Referer — allow (can't be browser-initiated CSRF)
|
|
69
85
|
if (!requestOrigin) return true;
|
|
70
86
|
|
|
71
|
-
// "null" origin comes from privacy-sensitive contexts (data: URLs,
|
|
72
|
-
// sandboxed iframes, cross-origin redirects). Reject it.
|
|
73
87
|
if (requestOrigin === "null") return false;
|
|
74
88
|
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
const
|
|
89
|
+
// An Origin/Referer is present, so this is a browser request worth checking.
|
|
90
|
+
// Establish the expected origin from the Host header only -- browsers always
|
|
91
|
+
// send Host alongside Origin (runtimes synthesize it from the HTTP/2
|
|
92
|
+
// :authority), so a missing Host here is anomalous. Fail closed rather than
|
|
93
|
+
// fall back to url.host (derived from the request line) when the trusted Host
|
|
94
|
+
// cannot be established.
|
|
95
|
+
const expectedHost = request.headers.get("host");
|
|
96
|
+
if (!expectedHost) return false;
|
|
82
97
|
|
|
83
|
-
|
|
84
|
-
const expectedOrigin = `${expectedProtocol}//${expectedHost}`;
|
|
98
|
+
const expectedOrigin = `${url.protocol}//${expectedHost}`;
|
|
85
99
|
|
|
86
100
|
return requestOrigin.toLowerCase() === expectedOrigin.toLowerCase();
|
|
87
101
|
}
|
|
@@ -116,14 +130,15 @@ export async function checkRequestOrigin<TEnv = any>(
|
|
|
116
130
|
// Disabled by explicit opt-out
|
|
117
131
|
if (config === false) return null;
|
|
118
132
|
|
|
119
|
-
// Default
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
// Default (true/undefined) becomes a callback returning boolean, so the
|
|
134
|
+
// Response|true|reject resolution below is written once.
|
|
135
|
+
const check: (
|
|
136
|
+
ctx: OriginCheckContext<TEnv>,
|
|
137
|
+
) => boolean | Response | Promise<boolean | Response> =
|
|
138
|
+
config === true || config === undefined
|
|
139
|
+
? () => defaultOriginCheck(request, url)
|
|
140
|
+
: config;
|
|
125
141
|
|
|
126
|
-
// Custom function — build context and call
|
|
127
142
|
const ctx: OriginCheckContext<TEnv> = {
|
|
128
143
|
request,
|
|
129
144
|
url,
|
|
@@ -133,9 +148,8 @@ export async function checkRequestOrigin<TEnv = any>(
|
|
|
133
148
|
defaultCheck: () => defaultOriginCheck(request, url),
|
|
134
149
|
};
|
|
135
150
|
|
|
136
|
-
const result = await
|
|
151
|
+
const result = await check(ctx);
|
|
137
152
|
|
|
138
153
|
if (result instanceof Response) return result;
|
|
139
|
-
|
|
140
|
-
return createForbiddenResponse(request);
|
|
154
|
+
return result === true ? null : createForbiddenResponse(request);
|
|
141
155
|
}
|
|
@@ -155,6 +155,14 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
155
155
|
} else if (isDirectAction && directActionId) {
|
|
156
156
|
const temporaryReferences = ctx.createTemporaryReferenceSet();
|
|
157
157
|
|
|
158
|
+
// INTENTIONAL JS/PE divergence (do NOT "fix" to match the JS reject path).
|
|
159
|
+
// On the JS path React Flight-encodes the action args, so decodeReply
|
|
160
|
+
// succeeds or a failure means a malformed body (rejected). On the no-JS PE
|
|
161
|
+
// path the browser submits a raw <form action={fn}> POST with NO encoded
|
|
162
|
+
// args, so decodeReply throws by design and the raw FormData IS the action
|
|
163
|
+
// argument (the React form-action convention: fn(formData)). Removing this
|
|
164
|
+
// fallback breaks every unbound no-JS form action (verified: it fails the
|
|
165
|
+
// progressive-enhancement dev+prod e2e suite). See #572 (decided: keep).
|
|
158
166
|
let args: unknown[] = [];
|
|
159
167
|
try {
|
|
160
168
|
args = await ctx.decodeReply(formData, { temporaryReferences });
|
|
@@ -254,11 +262,11 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
254
262
|
rootLayout: ctx.router.rootLayout,
|
|
255
263
|
handles: handleStore.stream(),
|
|
256
264
|
version: ctx.version,
|
|
265
|
+
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
257
266
|
themeConfig: ctx.router.themeConfig,
|
|
258
267
|
warmupEnabled: ctx.router.warmupEnabled,
|
|
259
268
|
initialTheme: requireRequestContext().theme,
|
|
260
269
|
},
|
|
261
|
-
formState: actionResult,
|
|
262
270
|
};
|
|
263
271
|
|
|
264
272
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
@@ -276,6 +284,8 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
276
284
|
url,
|
|
277
285
|
undefined,
|
|
278
286
|
);
|
|
287
|
+
// reactFormState carries the useActionState payload via the SSR-option path
|
|
288
|
+
// (renderToReadableStream({ formState })); it does NOT travel on RscPayload.
|
|
279
289
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
280
290
|
formState: reactFormState,
|
|
281
291
|
nonce,
|
|
@@ -362,6 +372,7 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
362
372
|
rootLayout: ctx.router.rootLayout,
|
|
363
373
|
handles: handleStore.stream(),
|
|
364
374
|
version: ctx.version,
|
|
375
|
+
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
365
376
|
themeConfig: ctx.router.themeConfig,
|
|
366
377
|
warmupEnabled: ctx.router.warmupEnabled,
|
|
367
378
|
initialTheme: requireRequestContext().theme,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side open-redirect guard.
|
|
3
|
+
*
|
|
4
|
+
* Applied to the FINAL handler response (the single top-level return in
|
|
5
|
+
* `handler.ts`) so every browser-followed redirect honors the same same-origin
|
|
6
|
+
* rule the client enforces (`browser/validate-redirect-origin.ts`), via the one
|
|
7
|
+
* shared resolver in `redirect-origin.ts`. This is the server half of the
|
|
8
|
+
* client's existing guard: the client can only validate redirects its own JS
|
|
9
|
+
* navigates to (the SPA/fetch channel), so document-native redirects -- a no-JS
|
|
10
|
+
* PE form POST, a full-page GET `match.redirect`, a middleware `redirect()`
|
|
11
|
+
* short-circuit, a response-route 3xx -- reach the browser with no client in the
|
|
12
|
+
* loop. They all funnel through one handler return, so guarding there covers
|
|
13
|
+
* every one and any future redirect exit.
|
|
14
|
+
*
|
|
15
|
+
* Soft (SPA/Flight) redirects are 200/204 responses (`X-RSC-Redirect` header or
|
|
16
|
+
* `metadata.redirect` payload) and are NOT redirect Responses, so they never
|
|
17
|
+
* reach this guard -- they stay validated client-side.
|
|
18
|
+
*
|
|
19
|
+
* Behavior on a `Location` header:
|
|
20
|
+
* - same-origin / relative -> passes through unchanged
|
|
21
|
+
* - `redirect(url, { external: true })` (out-of-band brand present) and an
|
|
22
|
+
* http(s) target -> allowed (explicit, auditable, unforgeable opt-in)
|
|
23
|
+
* - branded but a non-http(s) target (e.g. `javascript:`) -> neutralized: the
|
|
24
|
+
* opt-in waives the same-origin rule, NOT scheme safety
|
|
25
|
+
* - cross-origin without the brand -> Location rewritten to the basename root
|
|
26
|
+
* (a safe same-origin landing, the document analog of the client's "stay put");
|
|
27
|
+
* dev logs the blocked target and points to `{ external: true }`.
|
|
28
|
+
*
|
|
29
|
+
* The opt-in is an out-of-band brand on the Response object (isExternalRedirect),
|
|
30
|
+
* never a wire header: a header is forgeable by an attacker-controlled upstream
|
|
31
|
+
* response a proxy-style response route copies through, which would defeat the
|
|
32
|
+
* guard without app code ever opting in. The reserved header name is stripped
|
|
33
|
+
* defensively so a forged value can never reach the browser.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { isRedirectResponse } from "../response-utils.js";
|
|
37
|
+
import {
|
|
38
|
+
resolveSameOriginRedirect,
|
|
39
|
+
resolveExternalRedirect,
|
|
40
|
+
isExternalRedirect,
|
|
41
|
+
EXTERNAL_REDIRECT_MARKER,
|
|
42
|
+
} from "../redirect-origin.js";
|
|
43
|
+
import { carryOverRedirectHeaders } from "./helpers.js";
|
|
44
|
+
|
|
45
|
+
export function guardOutgoingRedirect(
|
|
46
|
+
response: Response,
|
|
47
|
+
requestOrigin: string,
|
|
48
|
+
basename: string | undefined,
|
|
49
|
+
): Response {
|
|
50
|
+
// Only 3xx + Location responses (document-native redirects) are guarded.
|
|
51
|
+
if (!isRedirectResponse(response)) {
|
|
52
|
+
return response;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The reserved marker is never a trust signal. Strip any value -- forged by a
|
|
56
|
+
// proxied upstream or otherwise -- so it can never reach the browser. Trust
|
|
57
|
+
// comes solely from the out-of-band brand below.
|
|
58
|
+
try {
|
|
59
|
+
response.headers.delete(EXTERNAL_REDIRECT_MARKER);
|
|
60
|
+
} catch {
|
|
61
|
+
// Some platform responses carry immutable headers; the header is inert on
|
|
62
|
+
// the browser, so a failed strip is harmless.
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// isRedirectResponse guarantees a truthy Location.
|
|
66
|
+
const location = response.headers.get("Location")!;
|
|
67
|
+
|
|
68
|
+
// Explicit opt-in via redirect(url, { external: true }): allow an off-host
|
|
69
|
+
// target, but only an http(s) one. external waives the same-origin rule, not
|
|
70
|
+
// scheme safety -- a branded javascript:/data: target falls through to be
|
|
71
|
+
// neutralized so it can never become a scriptable navigation downstream.
|
|
72
|
+
if (isExternalRedirect(response)) {
|
|
73
|
+
if (resolveExternalRedirect(location, requestOrigin) !== null) {
|
|
74
|
+
return response;
|
|
75
|
+
}
|
|
76
|
+
} else if (resolveSameOriginRedirect(location, requestOrigin) !== null) {
|
|
77
|
+
return response;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Cross-origin (or unsafe-scheme external): neutralize to a safe same-origin
|
|
81
|
+
// landing.
|
|
82
|
+
const safeTarget = basename && basename !== "/" ? basename : "/";
|
|
83
|
+
if (process.env.NODE_ENV !== "production") {
|
|
84
|
+
console.error(
|
|
85
|
+
`[rango] Blocked cross-origin redirect to "${location}"; sent to ` +
|
|
86
|
+
`"${safeTarget}" instead. To redirect off-host on purpose, use ` +
|
|
87
|
+
`redirect(url, { external: true }).`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const blocked = new Response(null, {
|
|
92
|
+
status: response.status,
|
|
93
|
+
headers: { Location: safeTarget },
|
|
94
|
+
});
|
|
95
|
+
// Preserve cookies and any other headers (Set-Cookie, Server-Timing, ...);
|
|
96
|
+
// carryOverRedirectHeaders intentionally skips Location.
|
|
97
|
+
carryOverRedirectHeaders(response, blocked);
|
|
98
|
+
return blocked;
|
|
99
|
+
}
|
|
@@ -1,37 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Problem Details (RFC 9457) Builder
|
|
3
3
|
*
|
|
4
|
-
* Builds a
|
|
5
|
-
*
|
|
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 {
|
|
9
|
+
import type { ProblemDetails } from "../urls.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
75
|
+
export function createProblemDetails(
|
|
17
76
|
error: unknown,
|
|
77
|
+
status: number,
|
|
18
78
|
isDev: boolean,
|
|
19
|
-
):
|
|
79
|
+
): ProblemDetails {
|
|
20
80
|
if (error instanceof RouterError) {
|
|
21
81
|
return {
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
+
title: statusPhrase(status),
|
|
100
|
+
status,
|
|
101
|
+
detail: isDev ? String(error) : "Internal Server Error",
|
|
102
|
+
code: "INTERNAL",
|
|
36
103
|
};
|
|
37
104
|
}
|
|
@@ -11,7 +11,8 @@ import { requireRequestContext } from "../server/request-context.js";
|
|
|
11
11
|
import { contextGet } from "../context-var.js";
|
|
12
12
|
import { NOCACHE_SYMBOL } from "../cache/taint.js";
|
|
13
13
|
import { traverseBack } from "../router/pattern-matching.js";
|
|
14
|
-
import {
|
|
14
|
+
import { RESPONSE_TYPE_MIME } from "../router/content-negotiation.js";
|
|
15
|
+
import { createCacheScope, resolveCacheTags } from "../cache/cache-scope.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 {
|
|
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;
|
|
@@ -109,49 +116,31 @@ export async function handleResponseRoute<TEnv>(
|
|
|
109
116
|
}
|
|
110
117
|
const headers = new Headers();
|
|
111
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;
|
|
112
124
|
if (key.toLowerCase() === "set-cookie") {
|
|
113
125
|
headers.append(key, value);
|
|
114
126
|
} else {
|
|
115
127
|
headers.set(key, value);
|
|
116
128
|
}
|
|
117
129
|
});
|
|
118
|
-
|
|
130
|
+
const rewrapped = createResponseWithMergedHeaders(result.body, {
|
|
119
131
|
status: result.status,
|
|
120
132
|
headers,
|
|
121
133
|
});
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (result instanceof Response) {
|
|
129
|
-
return rewrapResponse(result);
|
|
130
|
-
}
|
|
131
|
-
return createResponseWithMergedHeaders(
|
|
132
|
-
JSON.stringify({ data: result }),
|
|
133
|
-
{
|
|
134
|
-
status: 200,
|
|
135
|
-
headers: { "content-type": "application/json;charset=utf-8" },
|
|
136
|
-
},
|
|
137
|
-
);
|
|
138
|
-
} catch (error) {
|
|
139
|
-
handlerCtx.callOnError(error, "handler", errorCtx);
|
|
140
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
141
|
-
const status = error instanceof RouterError ? error.status : 500;
|
|
142
|
-
return createResponseWithMergedHeaders(
|
|
143
|
-
JSON.stringify({
|
|
144
|
-
error: createResponseErrorPayload(error, isDev),
|
|
145
|
-
}),
|
|
146
|
-
{
|
|
147
|
-
status,
|
|
148
|
-
headers: { "content-type": "application/json;charset=utf-8" },
|
|
149
|
-
},
|
|
150
|
-
);
|
|
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);
|
|
151
140
|
}
|
|
152
|
-
|
|
141
|
+
return rewrapped;
|
|
142
|
+
};
|
|
153
143
|
|
|
154
|
-
// Non-JSON response routes: catch errors and return plain Response
|
|
155
144
|
try {
|
|
156
145
|
const result = await (preview.handler as Function)(responseHandlerCtx);
|
|
157
146
|
|
|
@@ -159,38 +148,56 @@ export async function handleResponseRoute<TEnv>(
|
|
|
159
148
|
return rewrapResponse(result);
|
|
160
149
|
}
|
|
161
150
|
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
default:
|
|
185
|
-
// image, stream, any -- must return Response
|
|
186
|
-
throw new Error(
|
|
187
|
-
`Response route handler for "${preview.responseType}" must return a Response object, got ${typeof result}`,
|
|
188
|
-
);
|
|
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
|
+
});
|
|
189
173
|
}
|
|
174
|
+
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Response route handler for "${preview.responseType}" must return a Response object, got ${typeof result}`,
|
|
177
|
+
);
|
|
190
178
|
} catch (error) {
|
|
191
179
|
handlerCtx.callOnError(error, "handler", errorCtx);
|
|
192
180
|
const isDev = process.env.NODE_ENV !== "production";
|
|
193
|
-
const
|
|
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
|
+
|
|
194
201
|
const message =
|
|
195
202
|
error instanceof RouterError
|
|
196
203
|
? error.message
|
|
@@ -275,6 +282,11 @@ export async function handleResponseRoute<TEnv>(
|
|
|
275
282
|
}
|
|
276
283
|
}
|
|
277
284
|
|
|
285
|
+
// Resolve cache tags for this document entry (static or dynamic),
|
|
286
|
+
// while request context is available. Passed to putResponse so the
|
|
287
|
+
// entry is tag-invalidatable.
|
|
288
|
+
const responseTags = resolveCacheTags(cacheScope.config, reqCtx);
|
|
289
|
+
|
|
278
290
|
// Save pre-handler callbacks (registered by app-level middleware
|
|
279
291
|
// before we reach the cache block) and clear the live array.
|
|
280
292
|
// createResponseWithMergedHeaders (inside the handler) eagerly
|
|
@@ -316,6 +328,7 @@ export async function handleResponseRoute<TEnv>(
|
|
|
316
328
|
fresh.clone(),
|
|
317
329
|
cacheScope!.ttl,
|
|
318
330
|
cacheScope!.swr,
|
|
331
|
+
responseTags,
|
|
319
332
|
);
|
|
320
333
|
}
|
|
321
334
|
} catch (error) {
|
|
@@ -344,6 +357,7 @@ export async function handleResponseRoute<TEnv>(
|
|
|
344
357
|
response.clone(),
|
|
345
358
|
cacheScope!.ttl,
|
|
346
359
|
cacheScope!.swr,
|
|
360
|
+
responseTags,
|
|
347
361
|
);
|
|
348
362
|
} catch (error) {
|
|
349
363
|
console.error(`[ResponseCache] Cache write failed:`, error);
|