@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa
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/AGENTS.md +8 -4
- package/README.md +296 -887
- package/dist/bin/rango.js +459 -91
- package/dist/testing/vitest.js +36 -2
- package/dist/vite/index.js +1708 -414
- package/package.json +35 -10
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +82 -5
- package/skills/bundle-analysis/SKILL.md +2 -2
- package/skills/cache-guide/SKILL.md +14 -9
- package/skills/caching/SKILL.md +221 -12
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +5 -3
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +11 -3
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +9 -9
- package/skills/hooks/SKILL.md +73 -900
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +84 -7
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +51 -17
- package/skills/layout/SKILL.md +38 -16
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +48 -20
- package/skills/middleware/SKILL.md +11 -5
- package/skills/migrate-nextjs/SKILL.md +203 -20
- package/skills/migrate-react-router/SKILL.md +59 -675
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +3 -3
- package/skills/observability/SKILL.md +70 -5
- package/skills/parallel/SKILL.md +32 -8
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +59 -28
- package/skills/rango/SKILL.md +124 -50
- package/skills/response-routes/SKILL.md +78 -46
- package/skills/route/SKILL.md +85 -6
- package/skills/router-setup/SKILL.md +41 -6
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +28 -3
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +28 -4
- package/skills/testing/SKILL.md +68 -654
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +131 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +85 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -918
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +47 -17
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/__augment-tests__/augmented.check.ts +2 -3
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +178 -100
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +81 -68
- package/src/browser/navigation-client.ts +115 -70
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +153 -88
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +157 -144
- package/src/browser/prefetch/cache.ts +148 -81
- package/src/browser/prefetch/fetch.ts +231 -51
- package/src/browser/prefetch/queue.ts +25 -7
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +40 -7
- package/src/browser/react/NavigationProvider.tsx +140 -99
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +17 -2
- package/src/browser/react/index.ts +0 -51
- package/src/browser/react/location-state-shared.ts +14 -15
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +10 -5
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-router.ts +6 -4
- 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 +74 -8
- package/src/browser/rsc-router.tsx +97 -22
- package/src/browser/scroll-restoration.ts +15 -8
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +216 -38
- package/src/browser/types.ts +94 -22
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/generate-route-types.ts +1 -1
- package/src/build/index.ts +11 -5
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +152 -22
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +12 -1
- package/src/build/route-types/include-resolution.ts +455 -61
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +15 -2
- package/src/build/route-types/router-processing.ts +77 -41
- package/src/build/route-types/source-scan.ts +105 -7
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +58 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +454 -101
- package/src/cache/cache-scope.ts +159 -54
- package/src/cache/cache-tag.ts +149 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2170 -377
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +126 -41
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +243 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +13 -21
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +194 -99
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1132 -0
- package/src/client.rsc.tsx +39 -22
- package/src/client.tsx +28 -58
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +108 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +13 -1
- package/src/decode-loader-results.ts +18 -2
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +0 -3
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +57 -40
- package/src/handles/MetaTags.tsx +24 -53
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +35 -8
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +14 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +13 -22
- package/src/host/index.ts +7 -0
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +7 -12
- package/src/index.rsc.ts +88 -8
- package/src/index.ts +90 -16
- package/src/internal-debug.ts +11 -10
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +75 -7
- package/src/redirect-origin.ts +114 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +25 -0
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +13 -49
- package/src/route-definition/dsl-helpers.ts +60 -53
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +46 -46
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +6 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -20
- package/src/route-types.ts +0 -5
- package/src/router/content-negotiation.ts +58 -23
- package/src/router/error-handling.ts +44 -17
- package/src/router/find-match.ts +129 -30
- package/src/router/handler-context.ts +6 -1
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +35 -2
- package/src/router/lazy-includes.ts +79 -56
- package/src/router/loader-resolution.ts +151 -73
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +74 -40
- package/src/router/match-api.ts +76 -52
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +181 -178
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +115 -194
- package/src/router/match-middleware/cache-store.ts +61 -50
- 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 +36 -67
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +231 -120
- package/src/router/navigation-snapshot.ts +7 -56
- package/src/router/params-util.ts +23 -0
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +99 -152
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +111 -66
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +47 -42
- package/src/router/revalidation.ts +75 -81
- package/src/router/route-snapshot.ts +14 -3
- package/src/router/router-context.ts +6 -29
- package/src/router/router-interfaces.ts +70 -8
- package/src/router/router-options.ts +126 -4
- package/src/router/segment-resolution/fresh.ts +104 -80
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +155 -39
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +215 -304
- 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 +35 -15
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +160 -200
- package/src/router/telemetry.ts +9 -23
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +215 -0
- package/src/router/trie-matching.ts +171 -64
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +13 -5
- package/src/router.ts +119 -48
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +267 -152
- package/src/rsc/helpers.ts +78 -4
- package/src/rsc/index.ts +1 -4
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +29 -42
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/origin-guard.ts +11 -15
- package/src/rsc/progressive-enhancement.ts +120 -13
- package/src/rsc/redirect-guard.ts +100 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +58 -141
- package/src/rsc/rsc-rendering.ts +492 -49
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +268 -82
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +45 -3
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +31 -26
- package/src/segment-loader-promise.ts +49 -4
- package/src/segment-system.tsx +260 -95
- package/src/server/context.ts +99 -9
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +125 -2
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +379 -138
- package/src/ssr/index.tsx +491 -182
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +14 -31
- package/src/testing/dispatch.ts +533 -160
- package/src/testing/e2e/fixture.ts +45 -11
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/e2e/parity.ts +85 -4
- package/src/testing/e2e/server.ts +12 -0
- package/src/testing/flight-matchers.ts +7 -14
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +36 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +30 -0
- package/src/testing/flight.ts +145 -70
- package/src/testing/generated-routes.ts +26 -50
- package/src/testing/index.ts +18 -19
- package/src/testing/internal/context.ts +184 -68
- 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 +357 -0
- package/src/testing/render-route.tsx +134 -115
- package/src/testing/run-loader.ts +140 -51
- package/src/testing/run-middleware.ts +59 -33
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
- package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
- package/src/testing/vitest.ts +138 -16
- package/src/theme/ThemeProvider.tsx +56 -84
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- 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 +15 -15
- package/src/types/handler-context.ts +45 -15
- package/src/types/index.ts +2 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +8 -22
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +100 -13
- package/src/urls/include-helper.ts +10 -12
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -8
- package/src/urls/path-helper-types.ts +52 -14
- package/src/urls/path-helper.ts +5 -54
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +76 -42
- package/src/urls/urls-function.ts +0 -14
- package/src/use-loader.tsx +0 -186
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +34 -43
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/discovery/prerender-collection.ts +33 -46
- package/src/vite/discovery/state.ts +12 -1
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/index.ts +9 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +143 -10
- package/src/vite/plugins/cjs-to-esm.ts +8 -12
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +85 -56
- package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
- package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
- package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
- package/src/vite/plugins/expose-internal-ids.ts +10 -1
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +22 -27
- package/src/vite/plugins/version-plugin.ts +6 -66
- package/src/vite/plugins/virtual-entries.ts +137 -26
- package/src/vite/rango.ts +146 -135
- package/src/vite/router-discovery.ts +189 -48
- package/src/vite/utils/ast-handler-extract.ts +11 -20
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +4 -75
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +71 -44
- package/src/vite/utils/shared-utils.ts +55 -37
- 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/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -7,12 +7,15 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
getRequestContext,
|
|
11
11
|
setRequestContextParams,
|
|
12
12
|
} from "../server/request-context.js";
|
|
13
13
|
import { getSSRSetup } from "./ssr-setup.js";
|
|
14
14
|
import type { MiddlewareFn } from "../router/middleware.js";
|
|
15
15
|
import { executeMiddleware } from "../router/middleware.js";
|
|
16
|
+
import { observePhase, PHASES } from "../router/instrument.js";
|
|
17
|
+
import { gateTransitions } from "./transition-gate.js";
|
|
18
|
+
import { resolvedHandleStream } from "../handles/deferred-resolution.js";
|
|
16
19
|
import type { RscPayload, ReactFormState } from "./types.js";
|
|
17
20
|
import {
|
|
18
21
|
createResponseWithMergedHeaders,
|
|
@@ -44,7 +47,7 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
44
47
|
env: TEnv,
|
|
45
48
|
url: URL,
|
|
46
49
|
isAction: boolean,
|
|
47
|
-
handleStore: ReturnType<typeof
|
|
50
|
+
handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
|
|
48
51
|
nonce: string | undefined,
|
|
49
52
|
routeMwInfo?: PeRouteMiddlewareInfo,
|
|
50
53
|
): Promise<Response | null> {
|
|
@@ -115,6 +118,13 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
115
118
|
// Execute action and return HTML
|
|
116
119
|
let actionResult: unknown = undefined;
|
|
117
120
|
let reactFormState: ReactFormState | null = null;
|
|
121
|
+
// Status for the fall-through re-render after a boundaryless action error.
|
|
122
|
+
// When an action throws and NO error boundary matches, the PE path re-renders
|
|
123
|
+
// the page (below) the same way it would after a successful action. Without
|
|
124
|
+
// this the re-render serves HTTP 200, diverging from the JS path which serves
|
|
125
|
+
// 500 (server-action.ts sets actionStatus=500 for the same boundaryless error).
|
|
126
|
+
// 500 is carried only into the final HTML response, never the redirect branch.
|
|
127
|
+
let boundarylessErrorStatus: number | undefined;
|
|
118
128
|
|
|
119
129
|
if (isUseActionState) {
|
|
120
130
|
// Decode and extract action identity before execution so error
|
|
@@ -124,7 +134,11 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
124
134
|
const boundAction = await ctx.decodeAction(formData);
|
|
125
135
|
// React's custom .bind() preserves $$id on server references.
|
|
126
136
|
useActionStateId = (boundAction as { $$id?: string }).$$id ?? undefined;
|
|
127
|
-
|
|
137
|
+
// Meter the no-JS form action as the action phase, same as the JS path.
|
|
138
|
+
actionResult = await observePhase(
|
|
139
|
+
PHASES.action(useActionStateId ?? "useActionState"),
|
|
140
|
+
() => boundAction(),
|
|
141
|
+
);
|
|
128
142
|
} catch (error) {
|
|
129
143
|
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
130
144
|
const redirectResponse = extractRedirectResponse(error);
|
|
@@ -140,6 +154,7 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
140
154
|
handleStore,
|
|
141
155
|
nonce,
|
|
142
156
|
useActionStateId,
|
|
157
|
+
true, // an action ran and threw
|
|
143
158
|
);
|
|
144
159
|
if (errorHtml) return errorHtml;
|
|
145
160
|
|
|
@@ -151,10 +166,21 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
151
166
|
handledByBoundary: false,
|
|
152
167
|
});
|
|
153
168
|
console.error("[RSC] Progressive enhancement action error:", error);
|
|
169
|
+
// No boundary matched — the fall-through re-render must carry 500 to match
|
|
170
|
+
// the JS path's boundaryless-error status (server-action.ts).
|
|
171
|
+
boundarylessErrorStatus = 500;
|
|
154
172
|
}
|
|
155
173
|
} else if (isDirectAction && directActionId) {
|
|
156
174
|
const temporaryReferences = ctx.createTemporaryReferenceSet();
|
|
157
175
|
|
|
176
|
+
// INTENTIONAL JS/PE divergence (do NOT "fix" to match the JS reject path).
|
|
177
|
+
// On the JS path React Flight-encodes the action args, so decodeReply
|
|
178
|
+
// succeeds or a failure means a malformed body (rejected). On the no-JS PE
|
|
179
|
+
// path the browser submits a raw <form action={fn}> POST with NO encoded
|
|
180
|
+
// args, so decodeReply throws by design and the raw FormData IS the action
|
|
181
|
+
// argument (the React form-action convention: fn(formData)). Removing this
|
|
182
|
+
// fallback breaks every unbound no-JS form action (verified: it fails the
|
|
183
|
+
// progressive-enhancement dev+prod e2e suite). See #572 (decided: keep).
|
|
158
184
|
let args: unknown[] = [];
|
|
159
185
|
try {
|
|
160
186
|
args = await ctx.decodeReply(formData, { temporaryReferences });
|
|
@@ -164,7 +190,9 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
164
190
|
|
|
165
191
|
try {
|
|
166
192
|
const loadedAction = await ctx.loadServerAction(directActionId);
|
|
167
|
-
actionResult = await
|
|
193
|
+
actionResult = await observePhase(PHASES.action(directActionId), () =>
|
|
194
|
+
loadedAction.apply(null, args),
|
|
195
|
+
);
|
|
168
196
|
} catch (error) {
|
|
169
197
|
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
170
198
|
const redirectResponse = extractRedirectResponse(error);
|
|
@@ -180,6 +208,7 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
180
208
|
handleStore,
|
|
181
209
|
nonce,
|
|
182
210
|
directActionId,
|
|
211
|
+
true, // an action ran and threw
|
|
183
212
|
);
|
|
184
213
|
if (errorHtml) return errorHtml;
|
|
185
214
|
|
|
@@ -191,6 +220,9 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
191
220
|
handledByBoundary: false,
|
|
192
221
|
});
|
|
193
222
|
console.error("[RSC] Progressive enhancement action error:", error);
|
|
223
|
+
// No boundary matched — the fall-through re-render must carry 500 to match
|
|
224
|
+
// the JS path's boundaryless-error status (server-action.ts).
|
|
225
|
+
boundarylessErrorStatus = 500;
|
|
194
226
|
}
|
|
195
227
|
}
|
|
196
228
|
|
|
@@ -226,11 +258,29 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
226
258
|
// cookies set by route middleware are available during re-render — matching
|
|
227
259
|
// the behavior of JS-enabled requests.
|
|
228
260
|
const renderPage = async (): Promise<Response> => {
|
|
261
|
+
// Preserve the original POST request's headers (Authorization, Cookie,
|
|
262
|
+
// custom headers) so loaders that read request headers/cookies behave
|
|
263
|
+
// identically under PE and the JS action path. Drop body-framing headers
|
|
264
|
+
// from the bodyless GET and force the HTML accept.
|
|
265
|
+
const headers = new Headers(request.headers);
|
|
266
|
+
headers.delete("content-type");
|
|
267
|
+
headers.delete("content-length");
|
|
268
|
+
headers.delete("content-encoding");
|
|
269
|
+
headers.delete("transfer-encoding");
|
|
270
|
+
headers.set("accept", "text/html");
|
|
229
271
|
const renderRequest = new Request(url.toString(), {
|
|
230
272
|
method: "GET",
|
|
231
|
-
headers
|
|
273
|
+
headers,
|
|
232
274
|
});
|
|
233
275
|
|
|
276
|
+
// JS/PE parity: this is an action's revalidation render, so mark it BEFORE
|
|
277
|
+
// matching — a stale `foregroundOnAction` cache entry must re-execute in the
|
|
278
|
+
// foreground during the re-render, exactly as the JS path's
|
|
279
|
+
// revalidateAfterAction does. The transition({ when }) gate fields below are
|
|
280
|
+
// set post-match (the gate reads them after rendering); foregroundOnAction
|
|
281
|
+
// reads _inActionRevalidation during the match, so it must be set here.
|
|
282
|
+
getRequestContext()._inActionRevalidation = true;
|
|
283
|
+
|
|
234
284
|
const match = await ctx.router.match(renderRequest, { env });
|
|
235
285
|
|
|
236
286
|
if (match.redirect) {
|
|
@@ -240,25 +290,41 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
240
290
|
});
|
|
241
291
|
}
|
|
242
292
|
|
|
293
|
+
// Expose the no-JS action to the transition({ when }) gate. currentUrl/Params
|
|
294
|
+
// are absent on this full-render path (no navigation snapshot); useActionState
|
|
295
|
+
// ids are block-scoped, so only a direct action id is available here.
|
|
296
|
+
// actionUrl is the page the action was submitted from (this request's url).
|
|
297
|
+
const peReqCtx = getRequestContext();
|
|
298
|
+
peReqCtx._gateActionId = directActionId ?? undefined;
|
|
299
|
+
peReqCtx._gateActionUrl = new URL(url);
|
|
300
|
+
peReqCtx._gateActionResult = actionResult;
|
|
301
|
+
peReqCtx._gateFormData = formData;
|
|
302
|
+
|
|
243
303
|
const payload: RscPayload = {
|
|
244
304
|
metadata: {
|
|
245
305
|
pathname: url.pathname,
|
|
246
306
|
routerId: ctx.router.id,
|
|
247
307
|
basename: ctx.router.basename,
|
|
248
|
-
segments:
|
|
308
|
+
segments: gateTransitions(
|
|
309
|
+
match.segments,
|
|
310
|
+
getRequestContext(),
|
|
311
|
+
ctx.router.onError,
|
|
312
|
+
),
|
|
249
313
|
matched: match.matched,
|
|
250
314
|
diff: match.diff,
|
|
251
315
|
resolvedIds: match.resolvedIds,
|
|
252
316
|
params: match.params,
|
|
253
317
|
isPartial: false,
|
|
254
318
|
rootLayout: ctx.router.rootLayout,
|
|
255
|
-
|
|
319
|
+
// PE full render: resolve deferred handle values server-side.
|
|
320
|
+
handles: resolvedHandleStream(handleStore),
|
|
256
321
|
version: ctx.version,
|
|
322
|
+
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
257
323
|
themeConfig: ctx.router.themeConfig,
|
|
258
324
|
warmupEnabled: ctx.router.warmupEnabled,
|
|
259
|
-
|
|
325
|
+
strictMode: ctx.router.strictMode,
|
|
326
|
+
initialTheme: getRequestContext().theme,
|
|
260
327
|
},
|
|
261
|
-
formState: actionResult,
|
|
262
328
|
};
|
|
263
329
|
|
|
264
330
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
@@ -276,6 +342,8 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
276
342
|
url,
|
|
277
343
|
undefined,
|
|
278
344
|
);
|
|
345
|
+
// reactFormState carries the useActionState payload via the SSR-option path
|
|
346
|
+
// (renderToReadableStream({ formState })); it does NOT travel on RscPayload.
|
|
279
347
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
280
348
|
formState: reactFormState,
|
|
281
349
|
nonce,
|
|
@@ -283,6 +351,13 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
283
351
|
});
|
|
284
352
|
|
|
285
353
|
return createResponseWithMergedHeaders(htmlStream, {
|
|
354
|
+
// boundarylessErrorStatus is set only when the action threw and no error
|
|
355
|
+
// boundary matched; it makes the re-render carry 500 like the JS path.
|
|
356
|
+
// The redirect branch above returns before this, so a redirect re-render
|
|
357
|
+
// keeps its 308 and is never overridden.
|
|
358
|
+
...(boundarylessErrorStatus !== undefined
|
|
359
|
+
? { status: boundarylessErrorStatus }
|
|
360
|
+
: {}),
|
|
286
361
|
headers: { "content-type": "text/html;charset=utf-8" },
|
|
287
362
|
});
|
|
288
363
|
};
|
|
@@ -317,10 +392,25 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
317
392
|
env: TEnv,
|
|
318
393
|
url: URL,
|
|
319
394
|
error: unknown,
|
|
320
|
-
handleStore: ReturnType<typeof
|
|
395
|
+
handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
|
|
321
396
|
nonce: string | undefined,
|
|
322
397
|
actionId?: string | null,
|
|
398
|
+
// True when an action actually ran and threw (vs a malformed form body, where
|
|
399
|
+
// no action executed). Drives _inActionRevalidation for JS/PE parity — it must
|
|
400
|
+
// NOT be inferred from actionId, since a useActionState bound action can run
|
|
401
|
+
// and throw with no $$id (actionId === undefined) yet still be an action error.
|
|
402
|
+
actionRan = false,
|
|
323
403
|
): Promise<Response | null> {
|
|
404
|
+
// JS/PE parity for an action-triggered error re-render: a stale
|
|
405
|
+
// `foregroundOnAction` cache entry inside the error boundary must foreground
|
|
406
|
+
// too, exactly as the JS path (revalidateAfterAction sets this unconditionally
|
|
407
|
+
// before rendering the error boundary). Set BEFORE matchError (the cached fn
|
|
408
|
+
// runs during it). Gated on actionRan, NOT actionId — a malformed form body
|
|
409
|
+
// (actionRan=false) ran no action and must keep SWR.
|
|
410
|
+
if (actionRan) {
|
|
411
|
+
getRequestContext()._inActionRevalidation = true;
|
|
412
|
+
}
|
|
413
|
+
|
|
324
414
|
let errorResult;
|
|
325
415
|
try {
|
|
326
416
|
errorResult = await ctx.router.matchError(request, { env }, error, "route");
|
|
@@ -347,12 +437,26 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
347
437
|
|
|
348
438
|
setRequestContextParams(errorResult.params, errorResult.routeName);
|
|
349
439
|
|
|
440
|
+
// Only the failing action id + URL are in scope here (no formData/actionResult
|
|
441
|
+
// thread into this helper). Expose the URL only when the action id is known:
|
|
442
|
+
// this helper also handles malformed form bodies before action detection, and
|
|
443
|
+
// those should not look like action-triggered renders to transition({ when }).
|
|
444
|
+
if (actionId != null) {
|
|
445
|
+
const peErrCtx = getRequestContext();
|
|
446
|
+
peErrCtx._gateActionId = actionId;
|
|
447
|
+
peErrCtx._gateActionUrl = new URL(url);
|
|
448
|
+
}
|
|
449
|
+
|
|
350
450
|
const payload: RscPayload = {
|
|
351
451
|
metadata: {
|
|
352
452
|
pathname: url.pathname,
|
|
353
453
|
routerId: ctx.router.id,
|
|
354
454
|
basename: ctx.router.basename,
|
|
355
|
-
segments:
|
|
455
|
+
segments: gateTransitions(
|
|
456
|
+
errorResult.segments,
|
|
457
|
+
getRequestContext(),
|
|
458
|
+
ctx.router.onError,
|
|
459
|
+
),
|
|
356
460
|
matched: errorResult.matched,
|
|
357
461
|
diff: errorResult.diff,
|
|
358
462
|
resolvedIds: errorResult.resolvedIds,
|
|
@@ -360,11 +464,14 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
360
464
|
isPartial: false,
|
|
361
465
|
isError: true,
|
|
362
466
|
rootLayout: ctx.router.rootLayout,
|
|
363
|
-
|
|
467
|
+
// PE error-boundary full render: resolve deferred handle values server-side.
|
|
468
|
+
handles: resolvedHandleStream(handleStore),
|
|
364
469
|
version: ctx.version,
|
|
470
|
+
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
365
471
|
themeConfig: ctx.router.themeConfig,
|
|
366
472
|
warmupEnabled: ctx.router.warmupEnabled,
|
|
367
|
-
|
|
473
|
+
strictMode: ctx.router.strictMode,
|
|
474
|
+
initialTheme: getRequestContext().theme,
|
|
368
475
|
},
|
|
369
476
|
};
|
|
370
477
|
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
safeSameOriginLanding,
|
|
42
|
+
EXTERNAL_REDIRECT_MARKER,
|
|
43
|
+
} from "../redirect-origin.js";
|
|
44
|
+
import { carryOverRedirectHeaders } from "./helpers.js";
|
|
45
|
+
|
|
46
|
+
export function guardOutgoingRedirect(
|
|
47
|
+
response: Response,
|
|
48
|
+
requestOrigin: string,
|
|
49
|
+
basename: string | undefined,
|
|
50
|
+
): Response {
|
|
51
|
+
// Only 3xx + Location responses (document-native redirects) are guarded.
|
|
52
|
+
if (!isRedirectResponse(response)) {
|
|
53
|
+
return response;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The reserved marker is never a trust signal. Strip any value -- forged by a
|
|
57
|
+
// proxied upstream or otherwise -- so it can never reach the browser. Trust
|
|
58
|
+
// comes solely from the out-of-band brand below.
|
|
59
|
+
try {
|
|
60
|
+
response.headers.delete(EXTERNAL_REDIRECT_MARKER);
|
|
61
|
+
} catch {
|
|
62
|
+
// Some platform responses carry immutable headers; the header is inert on
|
|
63
|
+
// the browser, so a failed strip is harmless.
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// isRedirectResponse guarantees a truthy Location.
|
|
67
|
+
const location = response.headers.get("Location")!;
|
|
68
|
+
|
|
69
|
+
// Explicit opt-in via redirect(url, { external: true }): allow an off-host
|
|
70
|
+
// target, but only an http(s) one. external waives the same-origin rule, not
|
|
71
|
+
// scheme safety -- a branded javascript:/data: target falls through to be
|
|
72
|
+
// neutralized so it can never become a scriptable navigation downstream.
|
|
73
|
+
if (isExternalRedirect(response)) {
|
|
74
|
+
if (resolveExternalRedirect(location, requestOrigin) !== null) {
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
} else if (resolveSameOriginRedirect(location, requestOrigin) !== null) {
|
|
78
|
+
return response;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Cross-origin (or unsafe-scheme external): neutralize to a safe same-origin
|
|
82
|
+
// landing.
|
|
83
|
+
const safeTarget = safeSameOriginLanding(basename);
|
|
84
|
+
if (process.env.NODE_ENV !== "production") {
|
|
85
|
+
console.error(
|
|
86
|
+
`[rango] Blocked cross-origin redirect to "${location}"; sent to ` +
|
|
87
|
+
`"${safeTarget}" instead. To redirect off-host on purpose, use ` +
|
|
88
|
+
`redirect(url, { external: true }).`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const blocked = new Response(null, {
|
|
93
|
+
status: response.status,
|
|
94
|
+
headers: { Location: safeTarget },
|
|
95
|
+
});
|
|
96
|
+
// Preserve cookies and any other headers (Set-Cookie, Server-Timing, ...);
|
|
97
|
+
// carryOverRedirectHeaders intentionally skips Location.
|
|
98
|
+
carryOverRedirectHeaders(response, blocked);
|
|
99
|
+
return blocked;
|
|
100
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared response-route cache serve.
|
|
3
|
+
*
|
|
4
|
+
* Owns the single response-cache contract — cache-scope resolution from the
|
|
5
|
+
* matched entry tree, condition eval, key resolution (route key() > store
|
|
6
|
+
* keyGenerator > default), tag resolution, pre-handler-callback timing, and the
|
|
7
|
+
* fresh-hit / SWR-revalidate / miss-write branches — for BOTH the production
|
|
8
|
+
* response-route handler (rsc/response-route-handler.ts) and the dispatch testing
|
|
9
|
+
* primitive (testing/dispatch.ts), so the two can never drift.
|
|
10
|
+
*
|
|
11
|
+
* Plugin-rsc hazard: cache-scope.ts pulls @vitejs/plugin-rsc (via segment-codec),
|
|
12
|
+
* which the non-Vite unit-test runner cannot resolve, and this module is on the
|
|
13
|
+
* testing barrel's EAGER graph (dispatch imports it). So `createCacheScope` and
|
|
14
|
+
* `resolveCacheTags` are NOT imported here at runtime — they are INJECTED by the
|
|
15
|
+
* caller (production imports them statically; dispatch lazy-imports them only once
|
|
16
|
+
* a response route matches). The only runtime imports here are plugin-rsc-free
|
|
17
|
+
* (helpers' isCacheableStatus/finalizeResponse, traverseBack); cache-scope is a
|
|
18
|
+
* type-only import (erased at build).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { CacheScope } from "../cache/cache-scope.js";
|
|
22
|
+
import type { PartialCacheOptions } from "../types.js";
|
|
23
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
24
|
+
import type { SegmentCacheStore } from "../cache/types.js";
|
|
25
|
+
import type { EntryCacheConfig, EntryData } from "../server/context.js";
|
|
26
|
+
import { traverseBack } from "../router/pattern-matching.js";
|
|
27
|
+
import { isCacheableStatus, finalizeResponse } from "./helpers.js";
|
|
28
|
+
import { reportCacheError } from "../cache/cache-error.js";
|
|
29
|
+
|
|
30
|
+
/** Injected cache-scope builders (kept off this module's runtime import graph). */
|
|
31
|
+
export interface CacheScopeDeps {
|
|
32
|
+
createCacheScope: (
|
|
33
|
+
config: EntryCacheConfig | undefined,
|
|
34
|
+
parent?: CacheScope | null,
|
|
35
|
+
) => CacheScope | null;
|
|
36
|
+
resolveCacheTags: (
|
|
37
|
+
config: PartialCacheOptions | false,
|
|
38
|
+
ctx: RequestContext | undefined,
|
|
39
|
+
) => string[] | undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ServeResponseRouteWithCacheArgs {
|
|
43
|
+
reqCtx: RequestContext;
|
|
44
|
+
manifestEntry: EntryData;
|
|
45
|
+
responseType: string;
|
|
46
|
+
url: URL;
|
|
47
|
+
/** callHandler wrapped by route-level middleware — the unit the cache wraps. */
|
|
48
|
+
executeHandler: () => Promise<Response>;
|
|
49
|
+
deps: CacheScopeDeps;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Serve a response route through its cache, or return `undefined` when no cache
|
|
54
|
+
* applies (no scope, disabled, condition false, or store lacks get/putResponse)
|
|
55
|
+
* so the caller falls through to a plain `executeHandler()` run.
|
|
56
|
+
*
|
|
57
|
+
* Must run inside runWithRequestContext (reads the ambient request context via
|
|
58
|
+
* the helpers and reqCtx.waitUntil for background writes).
|
|
59
|
+
*/
|
|
60
|
+
export async function serveResponseRouteWithCache(
|
|
61
|
+
args: ServeResponseRouteWithCacheArgs,
|
|
62
|
+
): Promise<Response | undefined> {
|
|
63
|
+
const { reqCtx, manifestEntry, responseType, url, executeHandler, deps } =
|
|
64
|
+
args;
|
|
65
|
+
|
|
66
|
+
let cacheScope: CacheScope | null = null;
|
|
67
|
+
for (const entry of traverseBack(manifestEntry)) {
|
|
68
|
+
if (entry.cache) {
|
|
69
|
+
cacheScope = deps.createCacheScope(entry.cache, cacheScope);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!cacheScope?.enabled) return undefined;
|
|
74
|
+
|
|
75
|
+
// Evaluate condition — skip the response cache when condition returns false.
|
|
76
|
+
let conditionPassed = true;
|
|
77
|
+
if (cacheScope.config !== false && cacheScope.config.condition) {
|
|
78
|
+
try {
|
|
79
|
+
conditionPassed = !!cacheScope.config.condition(reqCtx);
|
|
80
|
+
} catch {
|
|
81
|
+
conditionPassed = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const store = cacheScope.getStore() ?? reqCtx._cacheStore;
|
|
86
|
+
if (!conditionPassed || !store?.getResponse || !store?.putResponse) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Build cache key with the response:{type}: prefix (avoids collision with
|
|
91
|
+
// segment keys); include host + url.search so query-driven and multi-host
|
|
92
|
+
// responses cache separately.
|
|
93
|
+
let cacheKey = `response:${responseType}:${url.host}${url.pathname}${url.search}`;
|
|
94
|
+
|
|
95
|
+
// Priority 1: route-level key() (full override). Priority 2: store-level
|
|
96
|
+
// keyGenerator (modifies the default key).
|
|
97
|
+
//
|
|
98
|
+
// A CONFIGURED key()/keyGenerator that THROWS must DEGRADE TO A MISS, not fall
|
|
99
|
+
// back to the broad default key. The default key
|
|
100
|
+
// `response:${type}:${host}${path}${search}` is intentionally broad; if the
|
|
101
|
+
// configured key encodes tenant/user/auth state, falling back to the broad key
|
|
102
|
+
// would cache PERSONALIZED output under it and serve it cross-user (cache
|
|
103
|
+
// poisoning). Mirrors the segment-cache behavior (cache-scope.ts lookupRoute):
|
|
104
|
+
// a throwing key degrades to a cache miss, never a collision onto the default
|
|
105
|
+
// slot. The no-key default path is left untouched (the broad key is correct
|
|
106
|
+
// when no key is configured).
|
|
107
|
+
let keyResolutionFailed = false;
|
|
108
|
+
if (cacheScope.config !== false && cacheScope.config.key) {
|
|
109
|
+
try {
|
|
110
|
+
const customKey = await cacheScope.config.key(reqCtx);
|
|
111
|
+
cacheKey = `response:${customKey}`;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
keyResolutionFailed = true;
|
|
114
|
+
reportCacheError(
|
|
115
|
+
error,
|
|
116
|
+
"cache-read",
|
|
117
|
+
"[ResponseCache] Key resolution failed",
|
|
118
|
+
reqCtx,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
} else if (store.keyGenerator) {
|
|
122
|
+
try {
|
|
123
|
+
cacheKey = await store.keyGenerator(reqCtx, cacheKey);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
keyResolutionFailed = true;
|
|
126
|
+
reportCacheError(
|
|
127
|
+
error,
|
|
128
|
+
"cache-read",
|
|
129
|
+
"[ResponseCache] keyGenerator failed",
|
|
130
|
+
reqCtx,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Degrade to a MISS: return undefined so the caller runs the route UNCACHED.
|
|
136
|
+
// This early-returns BEFORE _onResponseCallbacks is saved/cleared below, so the
|
|
137
|
+
// pre-handler onResponse callbacks are still intact for the uncached run.
|
|
138
|
+
if (keyResolutionFailed) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Resolve cache tags for this document entry (static or dynamic) while the
|
|
143
|
+
// request context is available, so the stored entry is tag-invalidatable.
|
|
144
|
+
const responseTags = deps.resolveCacheTags(cacheScope.config, reqCtx);
|
|
145
|
+
|
|
146
|
+
// Pre-handler callbacks (registered by app-level middleware before the cache
|
|
147
|
+
// block) are saved and the live array is cleared:
|
|
148
|
+
// createResponseWithMergedHeaders inside the handler eagerly drains whatever is
|
|
149
|
+
// in _onResponseCallbacks, so handler-registered callbacks bake into the cached
|
|
150
|
+
// artifact, while these pre-handler callbacks are applied once per serve on
|
|
151
|
+
// every path (hit + miss).
|
|
152
|
+
const savedCallbacks = reqCtx._onResponseCallbacks;
|
|
153
|
+
reqCtx._onResponseCallbacks = [];
|
|
154
|
+
const applyPreHandlerCallbacks = (response: Response): Response => {
|
|
155
|
+
let result = response;
|
|
156
|
+
for (const callback of savedCallbacks) {
|
|
157
|
+
result = callback(result) ?? result;
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const putFresh = (
|
|
163
|
+
store2: SegmentCacheStore,
|
|
164
|
+
fresh: Response,
|
|
165
|
+
): Promise<void> =>
|
|
166
|
+
store2.putResponse!(
|
|
167
|
+
cacheKey,
|
|
168
|
+
fresh.clone(),
|
|
169
|
+
cacheScope!.ttl,
|
|
170
|
+
cacheScope!.swr,
|
|
171
|
+
responseTags,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const cached = await store.getResponse(cacheKey);
|
|
176
|
+
if (cached && isCacheableStatus(cached.response.status)) {
|
|
177
|
+
if (!cached.shouldRevalidate) {
|
|
178
|
+
return applyPreHandlerCallbacks(cached.response);
|
|
179
|
+
}
|
|
180
|
+
// Stale hit (SWR): return cached, revalidate in background.
|
|
181
|
+
reqCtx.waitUntil(async () => {
|
|
182
|
+
try {
|
|
183
|
+
const fresh = finalizeResponse(await executeHandler());
|
|
184
|
+
if (isCacheableStatus(fresh.status)) await putFresh(store, fresh);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
reportCacheError(
|
|
187
|
+
error,
|
|
188
|
+
"stale-revalidation",
|
|
189
|
+
"[ResponseCache] background revalidation",
|
|
190
|
+
reqCtx,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
return applyPreHandlerCallbacks(cached.response);
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
reportCacheError(
|
|
198
|
+
error,
|
|
199
|
+
"cache-read",
|
|
200
|
+
"[ResponseCache] Cache lookup failed",
|
|
201
|
+
reqCtx,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Cache miss: execute the handler and cache the result.
|
|
206
|
+
const response = finalizeResponse(await executeHandler());
|
|
207
|
+
if (isCacheableStatus(response.status)) {
|
|
208
|
+
// Clone SYNCHRONOUSLY here, before returning. The original `response` is
|
|
209
|
+
// handed back to the middleware chain, where mergeResponse rebuilds it as
|
|
210
|
+
// `new Response(response.body, ...)`. Deferring the clone into the waitUntil
|
|
211
|
+
// callback (putFresh(response), which clones inside the async body) raced
|
|
212
|
+
// that rebuild: the background clone() and the foreground body read could
|
|
213
|
+
// interleave and throw "Response body object should not be disturbed or
|
|
214
|
+
// locked" (a flaky 500). Teeing now keeps the returned body independent of
|
|
215
|
+
// the cache write. The SWR path above is unaffected (its `fresh` is created
|
|
216
|
+
// inside the background callback and never returned to the caller).
|
|
217
|
+
const toCache = response.clone();
|
|
218
|
+
reqCtx.waitUntil(async () => {
|
|
219
|
+
try {
|
|
220
|
+
await store.putResponse!(
|
|
221
|
+
cacheKey,
|
|
222
|
+
toCache,
|
|
223
|
+
cacheScope!.ttl,
|
|
224
|
+
cacheScope!.swr,
|
|
225
|
+
responseTags,
|
|
226
|
+
);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
reportCacheError(
|
|
229
|
+
error,
|
|
230
|
+
"cache-write",
|
|
231
|
+
"[ResponseCache] Cache write failed",
|
|
232
|
+
reqCtx,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return applyPreHandlerCallbacks(response);
|
|
238
|
+
}
|