@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.71
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 +9 -0
- package/README.md +942 -4
- package/dist/bin/rango.js +1689 -0
- package/dist/vite/index.js +4951 -930
- package/package.json +70 -60
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +334 -72
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +131 -8
- package/skills/layout/SKILL.md +100 -3
- package/skills/links/SKILL.md +92 -31
- package/skills/loader/SKILL.md +404 -44
- package/skills/middleware/SKILL.md +173 -34
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +204 -1
- package/skills/prerender/SKILL.md +685 -0
- package/skills/rango/SKILL.md +85 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +257 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +328 -89
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +92 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +296 -558
- package/src/browser/navigation-client.ts +179 -69
- package/src/browser/navigation-store.ts +73 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +328 -313
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +150 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +160 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +230 -74
- package/src/browser/react/NavigationProvider.tsx +87 -11
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +30 -126
- package/src/browser/react/use-href.tsx +2 -2
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +22 -63
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +76 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +214 -58
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +221 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +141 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +39 -0
- package/src/build/index.ts +13 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +418 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +618 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +342 -0
- package/src/cache/cache-scope.ts +167 -309
- package/src/cache/cf/cf-cache-store.ts +571 -17
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +105 -179
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +108 -2
- package/src/handle.ts +55 -29
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +119 -29
- package/src/index.rsc.ts +155 -19
- package/src/index.ts +223 -30
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +26 -157
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +186 -0
- package/src/prerender.ts +524 -0
- package/src/reverse.ts +351 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +982 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +434 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-definition.ts +1 -1428
- package/src/route-map-builder.ts +217 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +70 -8
- package/src/router/content-negotiation.ts +215 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +435 -86
- package/src/router/intercept-resolution.ts +402 -0
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +356 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +154 -35
- package/src/router/match-api.ts +555 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +459 -10
- package/src/router/match-middleware/cache-store.ts +98 -26
- package/src/router/match-middleware/intercept-resolution.ts +57 -17
- package/src/router/match-middleware/segment-resolution.ts +80 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +135 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +220 -0
- package/src/router/middleware.ts +324 -369
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +211 -43
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +748 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1379 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +78 -3
- package/src/router.ts +740 -4252
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +907 -797
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +391 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +246 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +356 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +46 -11
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +165 -17
- package/src/server/context.ts +315 -58
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +607 -81
- package/src/server.ts +35 -130
- package/src/ssr/index.tsx +103 -30
- package/src/static-handler.ts +126 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +791 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +210 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +109 -0
- package/src/types/segments.ts +151 -0
- package/src/types.ts +1 -1623
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +346 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +116 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -802
- package/src/use-loader.tsx +161 -81
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +348 -0
- package/src/vite/discovery/prerender-collection.ts +439 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -1129
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +786 -0
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +462 -0
- package/src/vite/router-discovery.ts +918 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +207 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/href.ts +0 -255
- package/src/server/route-manifest-cache.ts +0 -173
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
package/src/rsc/handler.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference types="@vitejs/plugin-rsc/types" />
|
|
2
|
-
/// <reference path="../vite/version.d.ts" />
|
|
2
|
+
/// <reference path="../vite/plugins/version.d.ts" />
|
|
3
3
|
/**
|
|
4
4
|
* RSC Request Handler
|
|
5
5
|
*
|
|
@@ -8,36 +8,86 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { createElement } from "react";
|
|
11
|
-
import { renderSegments } from "../segment-system.js";
|
|
12
11
|
import { RouteNotFoundError } from "../errors.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
matchMiddleware,
|
|
16
|
-
executeMiddleware,
|
|
17
|
-
executeLoaderMiddleware,
|
|
18
|
-
} from "../router/middleware.js";
|
|
12
|
+
import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
|
|
19
13
|
import {
|
|
20
14
|
runWithRequestContext,
|
|
21
15
|
setRequestContextParams,
|
|
22
16
|
requireRequestContext,
|
|
17
|
+
getRequestContext,
|
|
18
|
+
_getRequestContext,
|
|
23
19
|
createRequestContext,
|
|
24
|
-
type ExecutionContext,
|
|
25
20
|
} from "../server/request-context.js";
|
|
26
21
|
import * as rscDeps from "@vitejs/plugin-rsc/rsc";
|
|
27
|
-
|
|
28
22
|
import type {
|
|
29
23
|
RscPayload,
|
|
30
|
-
ReactFormState,
|
|
31
24
|
CreateRSCHandlerOptions,
|
|
25
|
+
LoadSSRModule,
|
|
26
|
+
SSRModule,
|
|
32
27
|
} from "./types.js";
|
|
33
|
-
import {
|
|
34
|
-
|
|
28
|
+
import {
|
|
29
|
+
createResponseWithMergedHeaders,
|
|
30
|
+
finalizeResponse,
|
|
31
|
+
interceptRedirectForPartial,
|
|
32
|
+
buildRouteMiddlewareEntries,
|
|
33
|
+
} from "./helpers.js";
|
|
34
|
+
import {
|
|
35
|
+
handleResponseRoute,
|
|
36
|
+
type ResponseRouteMatch,
|
|
37
|
+
} from "./response-route-handler.js";
|
|
38
|
+
import { generateNonce, nonce as nonceToken } from "./nonce.js";
|
|
35
39
|
import { VERSION } from "@rangojs/router:version";
|
|
36
40
|
import type { ErrorPhase } from "../types.js";
|
|
41
|
+
import type { RouterRequestInput } from "../router/router-interfaces.js";
|
|
37
42
|
import { invokeOnError } from "../router/error-handling.js";
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
import {
|
|
44
|
+
createReverseFunction,
|
|
45
|
+
stripInternalParams,
|
|
46
|
+
} from "../router/handler-context.js";
|
|
47
|
+
import { getRouterContext } from "../router/router-context.js";
|
|
48
|
+
import { resolveSink, safeEmit } from "../router/telemetry.js";
|
|
49
|
+
import { contextSet } from "../context-var.js";
|
|
50
|
+
import {
|
|
51
|
+
hasCachedManifest,
|
|
52
|
+
getRouteTrie,
|
|
53
|
+
getPrecomputedEntries,
|
|
54
|
+
waitForManifestReady,
|
|
55
|
+
getRouterManifest,
|
|
56
|
+
getRouterTrie,
|
|
57
|
+
} from "../route-map-builder.js";
|
|
58
|
+
import type { HandlerContext } from "./handler-context.js";
|
|
59
|
+
import { buildRouterTrieFromUrlpatterns } from "./manifest-init.js";
|
|
60
|
+
import { handleProgressiveEnhancement } from "./progressive-enhancement.js";
|
|
61
|
+
import {
|
|
62
|
+
executeServerAction,
|
|
63
|
+
revalidateAfterAction,
|
|
64
|
+
type ActionContinuation,
|
|
65
|
+
} from "./server-action.js";
|
|
66
|
+
import { handleLoaderFetch } from "./loader-fetch.js";
|
|
67
|
+
import { checkRequestOrigin, type OriginCheckPhase } from "./origin-guard.js";
|
|
68
|
+
import { handleRscRendering } from "./rsc-rendering.js";
|
|
69
|
+
import {
|
|
70
|
+
withTimeout,
|
|
71
|
+
RouterTimeoutError,
|
|
72
|
+
createDefaultTimeoutResponse,
|
|
73
|
+
type TimeoutPhase,
|
|
74
|
+
} from "../router/timeout.js";
|
|
75
|
+
import {
|
|
76
|
+
createMetricsStore,
|
|
77
|
+
appendMetric,
|
|
78
|
+
buildMetricsTiming,
|
|
79
|
+
} from "../router/metrics.js";
|
|
80
|
+
import {
|
|
81
|
+
startSSRSetup,
|
|
82
|
+
getSSRSetup,
|
|
83
|
+
mayNeedSSR,
|
|
84
|
+
SSR_SETUP_VAR,
|
|
85
|
+
} from "./ssr-setup.js";
|
|
86
|
+
import {
|
|
87
|
+
classifyRequest,
|
|
88
|
+
type RequestPlan,
|
|
89
|
+
type ExecutableRequestPlan,
|
|
90
|
+
} from "../router/request-classification.js";
|
|
41
91
|
|
|
42
92
|
/**
|
|
43
93
|
* Create an RSC request handler.
|
|
@@ -89,31 +139,182 @@ export function createRSCHandler<
|
|
|
89
139
|
decodeFormState,
|
|
90
140
|
} = deps;
|
|
91
141
|
|
|
92
|
-
// Use provided loadSSRModule or default to vite RSC module loader
|
|
93
|
-
|
|
142
|
+
// Use provided loadSSRModule or default to vite RSC module loader.
|
|
143
|
+
// In production the SSR module is stable across requests, so memoize
|
|
144
|
+
// the dynamic import to avoid repeated module resolution overhead.
|
|
145
|
+
// In dev mode Vite may hot-reload the module, so skip memoization.
|
|
146
|
+
const rawLoadSSRModule: LoadSSRModule =
|
|
94
147
|
options.loadSSRModule ??
|
|
95
148
|
(() => import.meta.viteRsc.loadModule("ssr", "index"));
|
|
149
|
+
let _ssrModulePromise: Promise<SSRModule> | undefined;
|
|
150
|
+
const loadSSRModule: LoadSSRModule =
|
|
151
|
+
process.env.NODE_ENV === "production"
|
|
152
|
+
? () =>
|
|
153
|
+
(_ssrModulePromise ??= rawLoadSSRModule().catch((err) => {
|
|
154
|
+
_ssrModulePromise = undefined;
|
|
155
|
+
throw err;
|
|
156
|
+
}))
|
|
157
|
+
: rawLoadSSRModule;
|
|
96
158
|
|
|
97
159
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
160
|
+
* Per-request error reporter that deduplicates via the ALS request context.
|
|
161
|
+
*
|
|
162
|
+
* Uses the same _reportedErrors WeakSet as the router layer so errors
|
|
163
|
+
* that propagate across layers are only reported once per request.
|
|
100
164
|
*/
|
|
101
165
|
function callOnError(
|
|
102
166
|
error: unknown,
|
|
103
167
|
phase: ErrorPhase,
|
|
104
168
|
context: Parameters<typeof invokeOnError<TEnv>>[3],
|
|
105
169
|
): void {
|
|
170
|
+
// Guard: abort signal handlers fire asynchronously outside the ALS
|
|
171
|
+
// request scope, so the context may be gone. Skip dedup in that
|
|
172
|
+
// case — the error is from a cancelled stream, not a real failure.
|
|
173
|
+
const reqCtx = _getRequestContext();
|
|
174
|
+
if (error != null && typeof error === "object" && reqCtx) {
|
|
175
|
+
if (reqCtx._reportedErrors.has(error)) return;
|
|
176
|
+
reqCtx._reportedErrors.add(error);
|
|
177
|
+
}
|
|
106
178
|
invokeOnError(router.onError, error, phase, context, "RSC");
|
|
107
179
|
}
|
|
108
180
|
|
|
109
|
-
|
|
181
|
+
function getRequiredRouteMap(): Record<string, string> {
|
|
182
|
+
const routeMap = getRouterManifest(router.id);
|
|
183
|
+
if (!routeMap) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Route manifest for router "${router.id}" is not available.`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return routeMap;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Handle a timeout by reporting the error, emitting telemetry,
|
|
193
|
+
* and returning either the custom onTimeout response or a default 504.
|
|
194
|
+
*/
|
|
195
|
+
async function handleTimeoutResponse(
|
|
110
196
|
request: Request,
|
|
111
|
-
env: TEnv
|
|
112
|
-
|
|
197
|
+
env: TEnv,
|
|
198
|
+
url: URL,
|
|
199
|
+
phase: TimeoutPhase,
|
|
200
|
+
durationMs: number,
|
|
201
|
+
routeKey?: string,
|
|
202
|
+
actionId?: string,
|
|
203
|
+
): Promise<Response> {
|
|
204
|
+
const timeoutError = new RouterTimeoutError(phase, durationMs);
|
|
205
|
+
|
|
206
|
+
callOnError(timeoutError, phase === "action" ? "action" : "handler", {
|
|
207
|
+
request,
|
|
208
|
+
url,
|
|
209
|
+
env,
|
|
210
|
+
routeKey,
|
|
211
|
+
actionId,
|
|
212
|
+
handledByBoundary: false,
|
|
213
|
+
metadata: { timeout: true, phase, durationMs },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const routerCtx = getRouterContext();
|
|
218
|
+
if (routerCtx?.telemetry) {
|
|
219
|
+
safeEmit(resolveSink(routerCtx.telemetry), {
|
|
220
|
+
type: "request.timeout" as const,
|
|
221
|
+
timestamp: performance.now(),
|
|
222
|
+
requestId: routerCtx.requestId,
|
|
223
|
+
phase,
|
|
224
|
+
pathname: url.pathname,
|
|
225
|
+
routeKey,
|
|
226
|
+
actionId,
|
|
227
|
+
durationMs,
|
|
228
|
+
customHandler: !!router.onTimeout,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
// Router context may not be available
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (router.onTimeout) {
|
|
236
|
+
try {
|
|
237
|
+
return await router.onTimeout({
|
|
238
|
+
phase,
|
|
239
|
+
request,
|
|
240
|
+
url,
|
|
241
|
+
env,
|
|
242
|
+
routeKey,
|
|
243
|
+
actionId,
|
|
244
|
+
durationMs,
|
|
245
|
+
});
|
|
246
|
+
} catch (e) {
|
|
247
|
+
if (process.env.NODE_ENV !== "production") {
|
|
248
|
+
console.error("[RSC] onTimeout callback error:", e);
|
|
249
|
+
}
|
|
250
|
+
return createDefaultTimeoutResponse(phase);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return createDefaultTimeoutResponse(phase);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Build a 200 Flight response that carries a redirect URL and optional state.
|
|
259
|
+
* Used when a partial/action request results in a redirect -- fetch
|
|
260
|
+
* auto-follows 3xx so we send the redirect as payload metadata instead.
|
|
261
|
+
*/
|
|
262
|
+
function createRedirectFlightResponse(
|
|
263
|
+
redirectUrl: string,
|
|
264
|
+
locationState?: Record<string, unknown>,
|
|
265
|
+
): Response {
|
|
266
|
+
const redirectPayload: RscPayload = {
|
|
267
|
+
metadata: {
|
|
268
|
+
pathname: redirectUrl,
|
|
269
|
+
segments: [],
|
|
270
|
+
redirect: { url: redirectUrl },
|
|
271
|
+
...(locationState && { locationState }),
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
const rscStream = renderToReadableStream<RscPayload>(redirectPayload);
|
|
275
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
276
|
+
status: 200,
|
|
277
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Bundle shared dependencies for extracted handler functions.
|
|
282
|
+
// callOnError reads from ALS so it's inherently per-request scoped.
|
|
283
|
+
const handlerCtx: HandlerContext<TEnv> = {
|
|
284
|
+
router,
|
|
285
|
+
version,
|
|
286
|
+
renderToReadableStream,
|
|
287
|
+
decodeReply,
|
|
288
|
+
createTemporaryReferenceSet,
|
|
289
|
+
loadServerAction,
|
|
290
|
+
decodeAction,
|
|
291
|
+
decodeFormState,
|
|
292
|
+
loadSSRModule,
|
|
293
|
+
callOnError,
|
|
294
|
+
getRequiredRouteMap,
|
|
295
|
+
createRedirectFlightResponse,
|
|
296
|
+
resolveStreamMode: async (request, env, url) => {
|
|
297
|
+
const resolver = router.ssr?.resolveStreaming;
|
|
298
|
+
if (!resolver) return "stream";
|
|
299
|
+
return resolver({ request, env, url });
|
|
113
300
|
},
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
return async function handler(
|
|
304
|
+
request: Request,
|
|
305
|
+
input: RouterRequestInput<TEnv> = {},
|
|
114
306
|
): Promise<Response> {
|
|
307
|
+
const handlerStart = performance.now();
|
|
308
|
+
// Create the metrics store at handler start so handler:total has startTime=0
|
|
309
|
+
// and all metrics are relative to the request entry point.
|
|
310
|
+
const earlyMetricsStore = router.debugPerformance
|
|
311
|
+
? createMetricsStore(true, handlerStart)
|
|
312
|
+
: undefined;
|
|
313
|
+
|
|
314
|
+
const { env = {} as TEnv, vars: initialVars, ctx: executionCtx } = input;
|
|
315
|
+
|
|
115
316
|
// Connection warmup: return 204 immediately before any processing
|
|
116
|
-
if (router
|
|
317
|
+
if (router?.warmupEnabled && request.method === "HEAD") {
|
|
117
318
|
const warmupUrl = new URL(request.url);
|
|
118
319
|
if (warmupUrl.searchParams.has("_rsc_warmup")) {
|
|
119
320
|
return new Response(null, { status: 204 });
|
|
@@ -121,25 +322,30 @@ export function createRSCHandler<
|
|
|
121
322
|
}
|
|
122
323
|
|
|
123
324
|
// Resolve nonce if provider is set
|
|
325
|
+
const nonceStart = performance.now();
|
|
124
326
|
let nonce: string | undefined;
|
|
125
327
|
if (nonceProvider) {
|
|
126
328
|
const result = await nonceProvider(request, env);
|
|
127
329
|
nonce = result === true ? generateNonce() : result;
|
|
128
330
|
}
|
|
331
|
+
const nonceDur = performance.now() - nonceStart;
|
|
129
332
|
|
|
130
333
|
const url = new URL(request.url);
|
|
131
334
|
|
|
132
335
|
// Match global middleware
|
|
336
|
+
const mwMatchStart = performance.now();
|
|
133
337
|
const matchedMiddleware = matchMiddleware(url.pathname, router.middleware);
|
|
338
|
+
const mwMatchDur = performance.now() - mwMatchStart;
|
|
134
339
|
|
|
135
340
|
// Shared variables between middleware and route handlers
|
|
136
|
-
// Initialize from
|
|
137
|
-
const variables: Record<string, any> =
|
|
138
|
-
|
|
139
|
-
|
|
341
|
+
// Initialize from input.vars if provided (allows pre-seeding from worker entry)
|
|
342
|
+
const variables: Record<string, any> = initialVars
|
|
343
|
+
? { ...initialVars }
|
|
344
|
+
: {};
|
|
140
345
|
|
|
141
|
-
// Store nonce
|
|
346
|
+
// Store nonce via ContextVar token and string key for backward compat
|
|
142
347
|
if (nonce) {
|
|
348
|
+
contextSet(variables, nonceToken, nonce);
|
|
143
349
|
variables.nonce = nonce;
|
|
144
350
|
}
|
|
145
351
|
|
|
@@ -150,43 +356,103 @@ export function createRSCHandler<
|
|
|
150
356
|
const cacheOption = options.cache ?? router.cache;
|
|
151
357
|
if (cacheOption && !url.searchParams.has("__no_cache")) {
|
|
152
358
|
const cacheConfig =
|
|
153
|
-
typeof cacheOption === "function"
|
|
359
|
+
typeof cacheOption === "function"
|
|
360
|
+
? cacheOption(env, executionCtx)
|
|
361
|
+
: cacheOption;
|
|
154
362
|
|
|
155
363
|
if (cacheConfig.enabled !== false) {
|
|
156
364
|
cacheStore = cacheConfig.store;
|
|
157
365
|
}
|
|
158
366
|
}
|
|
159
367
|
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
368
|
+
// Route manifest is populated at startup via the virtual module
|
|
369
|
+
// (virtual:rsc-router/routes-manifest). In build/production, it's inlined
|
|
370
|
+
// into the bundle. In dev mode (Node), the discovery plugin populates it
|
|
371
|
+
// via setManifestReadyPromise(). In dev mode (Cloudflare), Miniflare runs
|
|
372
|
+
// in a separate isolate where module-level state doesn't carry over, so
|
|
373
|
+
// we generate inline from the router's urlpatterns.
|
|
374
|
+
//
|
|
375
|
+
// In multi-router setups (e.g. createHostRouter), each router must have
|
|
376
|
+
// its own per-router manifest. We check per-router data first: even if
|
|
377
|
+
// the global manifest was set by a different router, this router still
|
|
378
|
+
// needs its own trie and manifest for correct matching.
|
|
379
|
+
const manifestCacheStart = performance.now();
|
|
380
|
+
const hasRouterData = getRouterManifest(router.id) !== undefined;
|
|
381
|
+
if (!hasRouterData) {
|
|
382
|
+
if (!hasCachedManifest()) {
|
|
383
|
+
const readyPromise = waitForManifestReady();
|
|
384
|
+
if (readyPromise) {
|
|
385
|
+
await readyPromise;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (!getRouterManifest(router.id) && router.urlpatterns) {
|
|
389
|
+
// Cloudflare dev: generate manifest inline for this router.
|
|
390
|
+
// Each router generates its own manifest independently so
|
|
391
|
+
// multi-router setups (host routing) work correctly.
|
|
392
|
+
await buildRouterTrieFromUrlpatterns(router);
|
|
393
|
+
}
|
|
394
|
+
if (!getRouterManifest(router.id) && !hasCachedManifest()) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
'Route manifest not available. Ensure "virtual:rsc-router/routes-manifest" is imported in your entry file.',
|
|
397
|
+
);
|
|
398
|
+
}
|
|
173
399
|
}
|
|
174
400
|
|
|
175
|
-
//
|
|
176
|
-
// This
|
|
401
|
+
// Rebuild the trie when the manifest exists but the per-router trie is
|
|
402
|
+
// missing. This happens in dev mode after HMR: the virtual module sets
|
|
403
|
+
// the manifest (from fresh gen files) but the trie is intentionally not
|
|
404
|
+
// injected to avoid stale discovery-time data. Without the trie, route
|
|
405
|
+
// matching falls back to regex iteration which does not handle wildcard
|
|
406
|
+
// priority correctly (catch-all patterns match before specific routes).
|
|
407
|
+
if (!getRouterTrie(router.id) && router.urlpatterns) {
|
|
408
|
+
await buildRouterTrieFromUrlpatterns(router);
|
|
409
|
+
}
|
|
410
|
+
const manifestCacheDur = performance.now() - manifestCacheStart;
|
|
177
411
|
|
|
178
412
|
// Create unified request context with all methods
|
|
179
413
|
// Includes: stub response, handle store, loader memoization, use(), cookies, headers, cache store
|
|
180
414
|
// params starts empty, populated after route matching via setRequestContextParams
|
|
415
|
+
const ctxCreateStart = performance.now();
|
|
181
416
|
const requestContext = createRequestContext({
|
|
182
417
|
env,
|
|
183
418
|
request,
|
|
184
419
|
url,
|
|
185
420
|
variables,
|
|
186
421
|
cacheStore,
|
|
187
|
-
|
|
422
|
+
cacheProfiles: router.cacheProfiles,
|
|
423
|
+
executionContext: executionCtx,
|
|
188
424
|
themeConfig: router.themeConfig,
|
|
189
425
|
});
|
|
426
|
+
if (earlyMetricsStore) {
|
|
427
|
+
requestContext._debugPerformance = true;
|
|
428
|
+
requestContext._metricsStore = earlyMetricsStore;
|
|
429
|
+
}
|
|
430
|
+
// Wire background error reporting so "use cache" and other subsystems
|
|
431
|
+
// can surface non-fatal errors through the router's onError callback.
|
|
432
|
+
requestContext._reportBackgroundError = (
|
|
433
|
+
error: unknown,
|
|
434
|
+
category: string,
|
|
435
|
+
) => {
|
|
436
|
+
callOnError(error, "cache", {
|
|
437
|
+
request,
|
|
438
|
+
url,
|
|
439
|
+
metadata: { category },
|
|
440
|
+
});
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const ctxCreateDur = performance.now() - ctxCreateStart;
|
|
444
|
+
|
|
445
|
+
// Accumulate handler-level timing for Server-Timing header
|
|
446
|
+
const handlerTiming = [
|
|
447
|
+
`handler-nonce;dur=${nonceDur.toFixed(2)}`,
|
|
448
|
+
`handler-mw-match;dur=${mwMatchDur.toFixed(2)}`,
|
|
449
|
+
`handler-manifest-cache;dur=${manifestCacheDur.toFixed(2)}`,
|
|
450
|
+
`handler-ctx-create;dur=${ctxCreateDur.toFixed(2)}`,
|
|
451
|
+
];
|
|
452
|
+
|
|
453
|
+
// Store timing data in variables for downstream access
|
|
454
|
+
variables.__handlerTiming = handlerTiming;
|
|
455
|
+
variables.__handlerStart = handlerStart;
|
|
190
456
|
|
|
191
457
|
// Wrap entire request handling in request context
|
|
192
458
|
// Makes context available via getRequestContext() throughout:
|
|
@@ -195,6 +461,9 @@ export function createRSCHandler<
|
|
|
195
461
|
// - Server components during rendering
|
|
196
462
|
// - Error boundaries
|
|
197
463
|
// - Streaming
|
|
464
|
+
// Store basename on request context (scoped per-request via existing ALS)
|
|
465
|
+
requestContext._basename = router.basename;
|
|
466
|
+
|
|
198
467
|
return runWithRequestContext(requestContext, async () => {
|
|
199
468
|
// Core handler logic (wrapped by middleware)
|
|
200
469
|
const coreHandler = async (): Promise<Response> => {
|
|
@@ -202,21 +471,77 @@ export function createRSCHandler<
|
|
|
202
471
|
};
|
|
203
472
|
|
|
204
473
|
// Execute middleware chain if any, otherwise call core handler directly
|
|
474
|
+
let response: Response;
|
|
205
475
|
if (matchedMiddleware.length > 0) {
|
|
206
|
-
|
|
476
|
+
const mwResponse = await executeMiddleware(
|
|
207
477
|
matchedMiddleware,
|
|
208
478
|
request,
|
|
209
479
|
env,
|
|
210
480
|
variables,
|
|
211
481
|
coreHandler,
|
|
482
|
+
createReverseFunction(getRequiredRouteMap()),
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
if (
|
|
486
|
+
url.searchParams.has("_rsc_partial") ||
|
|
487
|
+
url.searchParams.has("_rsc_action")
|
|
488
|
+
) {
|
|
489
|
+
const intercepted = interceptRedirectForPartial(
|
|
490
|
+
mwResponse,
|
|
491
|
+
createRedirectFlightResponse,
|
|
492
|
+
);
|
|
493
|
+
response = intercepted ?? finalizeResponse(mwResponse);
|
|
494
|
+
} else {
|
|
495
|
+
response = finalizeResponse(mwResponse);
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
response = await coreHandler();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Finalize metrics after all middleware (including post-next work)
|
|
502
|
+
// has completed so :post spans are captured in the timeline.
|
|
503
|
+
// Handler timing parts are always emitted (even without debug metrics)
|
|
504
|
+
// so non-debug requests still get bootstrap Server-Timing entries.
|
|
505
|
+
const handlerTimingArr: string[] = variables.__handlerTiming || [];
|
|
506
|
+
// Preserve any existing Server-Timing set by response routes or middleware
|
|
507
|
+
const existingTiming = response.headers.get("Server-Timing");
|
|
508
|
+
const timingParts = existingTiming
|
|
509
|
+
? [existingTiming, ...handlerTimingArr]
|
|
510
|
+
: [...handlerTimingArr];
|
|
511
|
+
|
|
512
|
+
const metricsStore = requestContext._metricsStore;
|
|
513
|
+
if (metricsStore) {
|
|
514
|
+
// When the store was created at handler start (earlyMetricsStore),
|
|
515
|
+
// handler:total covers the full request. When ctx.debugPerformance()
|
|
516
|
+
// created the store mid-request, use its requestStart to avoid a
|
|
517
|
+
// negative startTime offset.
|
|
518
|
+
const totalStart = earlyMetricsStore
|
|
519
|
+
? handlerStart
|
|
520
|
+
: metricsStore.requestStart;
|
|
521
|
+
appendMetric(
|
|
522
|
+
metricsStore,
|
|
523
|
+
"handler:total",
|
|
524
|
+
totalStart,
|
|
525
|
+
performance.now() - totalStart,
|
|
526
|
+
);
|
|
527
|
+
const metricsTiming = buildMetricsTiming(
|
|
528
|
+
request.method,
|
|
529
|
+
url.pathname,
|
|
530
|
+
metricsStore,
|
|
212
531
|
);
|
|
532
|
+
if (metricsTiming) timingParts.push(metricsTiming);
|
|
213
533
|
}
|
|
214
534
|
|
|
215
|
-
|
|
535
|
+
const fullTiming = timingParts.join(", ");
|
|
536
|
+
if (fullTiming) response.headers.set("Server-Timing", fullTiming);
|
|
537
|
+
|
|
538
|
+
return response;
|
|
216
539
|
});
|
|
217
540
|
};
|
|
218
541
|
|
|
219
|
-
// Core request handling logic (separated for middleware wrapping)
|
|
542
|
+
// Core request handling logic (separated for middleware wrapping).
|
|
543
|
+
// Uses the classify → execute model: classifyRequest produces a RequestPlan,
|
|
544
|
+
// then execution dispatches on the plan mode.
|
|
220
545
|
async function coreRequestHandler(
|
|
221
546
|
request: Request,
|
|
222
547
|
env: TEnv,
|
|
@@ -224,856 +549,641 @@ export function createRSCHandler<
|
|
|
224
549
|
variables: Record<string, any>,
|
|
225
550
|
nonce: string | undefined,
|
|
226
551
|
): Promise<Response> {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
552
|
+
const handlerTiming: string[] = variables.__handlerTiming || [];
|
|
553
|
+
|
|
554
|
+
// Debug manifest endpoint: handled before classification since it
|
|
555
|
+
// doesn't need a route match and needs trie access from the closure.
|
|
556
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
557
|
+
if (
|
|
558
|
+
url.searchParams.has("__debug_manifest") &&
|
|
559
|
+
(isDev || router.allowDebugManifest)
|
|
560
|
+
) {
|
|
561
|
+
const trie = getRouterTrie(router.id) ?? getRouteTrie();
|
|
562
|
+
const routeManifest = getRequiredRouteMap();
|
|
563
|
+
const { extractAncestryFromTrie } =
|
|
564
|
+
await import("../build/route-trie.js");
|
|
565
|
+
return new Response(
|
|
566
|
+
JSON.stringify(
|
|
567
|
+
{
|
|
568
|
+
routerId: router.id,
|
|
569
|
+
routeManifest,
|
|
570
|
+
routeAncestry: trie ? extractAncestryFromTrie(trie) : {},
|
|
571
|
+
routeTrie: trie,
|
|
572
|
+
precomputedEntries: getPrecomputedEntries(),
|
|
573
|
+
},
|
|
574
|
+
null,
|
|
575
|
+
2,
|
|
576
|
+
),
|
|
577
|
+
{
|
|
578
|
+
headers: { "Content-Type": "application/json" },
|
|
238
579
|
},
|
|
239
|
-
params: mw.params,
|
|
240
|
-
}));
|
|
241
|
-
|
|
242
|
-
// Execute route middleware wrapping the actual request handling
|
|
243
|
-
return executeMiddleware(middlewareEntries, request, env, variables, () =>
|
|
244
|
-
coreRequestHandlerInner(request, env, url, variables, nonce),
|
|
245
580
|
);
|
|
246
581
|
}
|
|
247
582
|
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
): Promise<Response> {
|
|
260
|
-
// Early return for static file requests that don't need RSC handling
|
|
261
|
-
if (url.pathname === "/favicon.ico" || url.pathname === "/robots.txt") {
|
|
262
|
-
return new Response(null, { status: 404 });
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Debug endpoint - only in development
|
|
266
|
-
if (url.pathname === "/__debug_manifest" && process.env.NODE_ENV !== "production") {
|
|
267
|
-
const manifest = await router.debugManifest();
|
|
268
|
-
return new Response(JSON.stringify(manifest, null, 2), {
|
|
269
|
-
headers: { "Content-Type": "application/json" },
|
|
583
|
+
// ---- 1. Classify ----
|
|
584
|
+
// classifyRequest may throw RouteNotFoundError for unknown routes.
|
|
585
|
+
// In that case, fall through to a full-render plan so the pipeline
|
|
586
|
+
// can render the 404 page via the existing error handling path.
|
|
587
|
+
const classifyStart = performance.now();
|
|
588
|
+
let plan: RequestPlan<TEnv>;
|
|
589
|
+
try {
|
|
590
|
+
plan = await classifyRequest<TEnv>(request, url, {
|
|
591
|
+
findMatch: router.findMatch,
|
|
592
|
+
routerVersion: version,
|
|
593
|
+
routerId: router.id,
|
|
270
594
|
});
|
|
595
|
+
} catch (error) {
|
|
596
|
+
if (
|
|
597
|
+
error instanceof RouteNotFoundError ||
|
|
598
|
+
(error instanceof Error && error.name === "RouteNotFoundError")
|
|
599
|
+
) {
|
|
600
|
+
// Let the render path handle 404 — match()/matchPartial() will
|
|
601
|
+
// re-throw RouteNotFoundError and the catch block in
|
|
602
|
+
// executeRenderWithMiddleware renders the not-found page.
|
|
603
|
+
plan = {
|
|
604
|
+
mode: "full-render",
|
|
605
|
+
route: {
|
|
606
|
+
matched: null as any,
|
|
607
|
+
manifestEntry: null as any,
|
|
608
|
+
entries: [],
|
|
609
|
+
routeKey: "",
|
|
610
|
+
localRouteName: "",
|
|
611
|
+
params: {},
|
|
612
|
+
routeMiddleware: [],
|
|
613
|
+
cacheScope: null,
|
|
614
|
+
isPassthrough: false,
|
|
615
|
+
},
|
|
616
|
+
negotiated: false,
|
|
617
|
+
};
|
|
618
|
+
} else {
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const classifyDur = performance.now() - classifyStart;
|
|
623
|
+
handlerTiming.push(`handler-classify;dur=${classifyDur.toFixed(2)}`);
|
|
624
|
+
|
|
625
|
+
// ---- 2. Terminal plans (no execution needed) ----
|
|
626
|
+
if (plan.mode === "redirect") {
|
|
627
|
+
// Redirects are handled by the pipeline (match/matchPartial),
|
|
628
|
+
// but for partial requests we short-circuit with a Flight redirect.
|
|
629
|
+
if (url.searchParams.has("_rsc_partial")) {
|
|
630
|
+
return createRedirectFlightResponse(plan.redirectUrl);
|
|
631
|
+
}
|
|
632
|
+
// Full requests: let the pipeline handle the redirect via match()
|
|
633
|
+
// which returns { redirect: url }. Fall through to full-render.
|
|
271
634
|
}
|
|
272
635
|
|
|
273
|
-
|
|
274
|
-
const isAction =
|
|
275
|
-
request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
|
|
276
|
-
const actionId =
|
|
277
|
-
request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
|
|
278
|
-
|
|
279
|
-
// Version mismatch detection - client may have stale code after HMR/deployment
|
|
280
|
-
// If versions don't match, tell the client to reload
|
|
281
|
-
const clientVersion = url.searchParams.get("_rsc_v");
|
|
282
|
-
if (version && clientVersion && clientVersion !== version) {
|
|
636
|
+
if (plan.mode === "version-mismatch") {
|
|
283
637
|
console.log(
|
|
284
|
-
`[RSC] Version mismatch: client=${
|
|
638
|
+
`[RSC] Version mismatch: client=${url.searchParams.get("_rsc_v")}, server=${version}. Forcing reload.`,
|
|
285
639
|
);
|
|
286
|
-
|
|
287
|
-
// Clean URL by removing RSC params
|
|
288
|
-
const cleanUrl = new URL(url);
|
|
289
|
-
cleanUrl.searchParams.delete("_rsc_partial");
|
|
290
|
-
cleanUrl.searchParams.delete("_rsc_segments");
|
|
291
|
-
cleanUrl.searchParams.delete("_rsc_v");
|
|
292
|
-
cleanUrl.searchParams.delete("_rsc_stale");
|
|
293
|
-
cleanUrl.searchParams.delete("_rsc_action");
|
|
294
|
-
cleanUrl.searchParams.delete("_rsc_prev");
|
|
295
|
-
|
|
296
|
-
// For actions, reload current page (referer)
|
|
297
|
-
// For navigation, load the target URL
|
|
298
|
-
const reloadUrl = isAction
|
|
299
|
-
? request.headers.get("referer") || cleanUrl.toString()
|
|
300
|
-
: cleanUrl.toString();
|
|
301
|
-
|
|
302
|
-
// Return special response that tells client to reload
|
|
303
640
|
return createResponseWithMergedHeaders(null, {
|
|
304
641
|
status: 200,
|
|
305
642
|
headers: {
|
|
306
|
-
"X-RSC-Reload": reloadUrl,
|
|
643
|
+
"X-RSC-Reload": plan.reloadUrl,
|
|
307
644
|
"content-type": "text/x-component;charset=utf-8",
|
|
308
645
|
},
|
|
309
646
|
});
|
|
310
647
|
}
|
|
311
648
|
|
|
312
|
-
//
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
649
|
+
// ---- 3. Origin guard (gate for action/loader/PE modes) ----
|
|
650
|
+
const originPhase: OriginCheckPhase | null =
|
|
651
|
+
plan.mode === "action"
|
|
652
|
+
? "action"
|
|
653
|
+
: plan.mode === "loader"
|
|
654
|
+
? "loader"
|
|
655
|
+
: plan.mode === "pe-render"
|
|
656
|
+
? "pe-form"
|
|
657
|
+
: null;
|
|
658
|
+
if (originPhase) {
|
|
659
|
+
const originResult = await checkRequestOrigin(
|
|
320
660
|
request,
|
|
321
|
-
env,
|
|
322
661
|
url,
|
|
323
|
-
|
|
324
|
-
handleStore,
|
|
325
|
-
nonce,
|
|
326
|
-
);
|
|
327
|
-
if (progressiveResult) {
|
|
328
|
-
return progressiveResult;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// ============================================================================
|
|
332
|
-
// SERVER ACTION EXECUTION (JavaScript-enabled client)
|
|
333
|
-
// ============================================================================
|
|
334
|
-
if (isAction && actionId) {
|
|
335
|
-
return handleServerAction(request, env, url, actionId, handleStore);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// ============================================================================
|
|
339
|
-
// LOADER FETCH EXECUTION (data fetching with RSC serialization)
|
|
340
|
-
// ============================================================================
|
|
341
|
-
const isLoaderRequest = url.searchParams.has("_rsc_loader");
|
|
342
|
-
if (isLoaderRequest) {
|
|
343
|
-
return handleLoaderFetch(request, env, url, variables);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// ============================================================================
|
|
347
|
-
// REGULAR RSC RENDERING (Navigation)
|
|
348
|
-
// ============================================================================
|
|
349
|
-
// Note: Must use "return await" for try/catch to catch async rejections
|
|
350
|
-
return await handleRscRendering(
|
|
351
|
-
request,
|
|
662
|
+
router.originCheck,
|
|
352
663
|
env,
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
handleStore,
|
|
356
|
-
nonce,
|
|
664
|
+
router.id,
|
|
665
|
+
originPhase,
|
|
357
666
|
);
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
667
|
+
if (originResult) {
|
|
668
|
+
const originError = new Error(
|
|
669
|
+
`Origin check rejected: ${request.headers.get("origin") ?? "none"} vs ${request.headers.get("host") ?? "none"}`,
|
|
670
|
+
);
|
|
671
|
+
originError.name = "OriginCheckError";
|
|
363
672
|
|
|
364
|
-
|
|
365
|
-
// Check both instanceof and error.name for cross-bundle compatibility
|
|
366
|
-
const isRouteNotFound =
|
|
367
|
-
error instanceof RouteNotFoundError ||
|
|
368
|
-
(error instanceof Error && error.name === "RouteNotFoundError");
|
|
369
|
-
if (isRouteNotFound) {
|
|
370
|
-
callOnError(error, "routing", {
|
|
673
|
+
callOnError(originError, "origin", {
|
|
371
674
|
request,
|
|
372
675
|
url,
|
|
373
676
|
env,
|
|
374
|
-
handledByBoundary:
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// Get notFound component from router options or use default
|
|
378
|
-
const notFoundOption = router.notFound;
|
|
379
|
-
const notFoundComponent =
|
|
380
|
-
typeof notFoundOption === "function"
|
|
381
|
-
? notFoundOption({ pathname: url.pathname })
|
|
382
|
-
: (notFoundOption ?? createElement("h1", null, "Not Found"));
|
|
383
|
-
|
|
384
|
-
// Create a simple segment for the 404 page
|
|
385
|
-
const notFoundSegment = {
|
|
386
|
-
id: "notFound",
|
|
387
|
-
namespace: "notFound",
|
|
388
|
-
type: "route" as const,
|
|
389
|
-
index: 0,
|
|
390
|
-
component: notFoundComponent,
|
|
391
|
-
params: {},
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
// Render with rootLayout to maintain app shell
|
|
395
|
-
const root = await renderSegments([notFoundSegment], {
|
|
396
|
-
rootLayout: router.rootLayout,
|
|
397
|
-
// No routeName for not-found routes
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
const payload: RscPayload = {
|
|
401
|
-
root,
|
|
677
|
+
handledByBoundary: false,
|
|
402
678
|
metadata: {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
diff: [],
|
|
407
|
-
isPartial: false,
|
|
408
|
-
handles: handleStore.stream(),
|
|
409
|
-
version,
|
|
410
|
-
themeConfig: router.themeConfig,
|
|
411
|
-
warmupEnabled: router.warmupEnabled,
|
|
412
|
-
initialTheme: requireRequestContext().theme,
|
|
413
|
-
// No routeName for not-found routes
|
|
679
|
+
phase: originPhase,
|
|
680
|
+
origin: request.headers.get("origin"),
|
|
681
|
+
host: request.headers.get("host"),
|
|
414
682
|
},
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
const rscStream = renderToReadableStream(payload);
|
|
418
|
-
|
|
419
|
-
// Determine if this is an RSC request or HTML request
|
|
420
|
-
const isRscRequest =
|
|
421
|
-
(!request.headers.get("accept")?.includes("text/html") &&
|
|
422
|
-
!url.searchParams.has("__html")) ||
|
|
423
|
-
url.searchParams.has("__rsc");
|
|
683
|
+
});
|
|
424
684
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
685
|
+
try {
|
|
686
|
+
const routerCtx = getRouterContext();
|
|
687
|
+
if (routerCtx?.telemetry) {
|
|
688
|
+
safeEmit(resolveSink(routerCtx.telemetry), {
|
|
689
|
+
type: "request.origin-rejected" as const,
|
|
690
|
+
timestamp: performance.now(),
|
|
691
|
+
requestId: routerCtx.requestId,
|
|
692
|
+
method: request.method,
|
|
693
|
+
pathname: url.pathname,
|
|
694
|
+
phase: originPhase,
|
|
695
|
+
origin: request.headers.get("origin"),
|
|
696
|
+
host: request.headers.get("host"),
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
} catch {
|
|
700
|
+
// Router context may not be available
|
|
430
701
|
}
|
|
431
702
|
|
|
432
|
-
|
|
433
|
-
const ssrModule = await loadSSRModule();
|
|
434
|
-
const htmlStream = await ssrModule.renderHTML(rscStream, { nonce });
|
|
435
|
-
|
|
436
|
-
return createResponseWithMergedHeaders(htmlStream, {
|
|
437
|
-
status: 404,
|
|
438
|
-
headers: { "content-type": "text/html;charset=utf-8" },
|
|
439
|
-
});
|
|
703
|
+
return originResult;
|
|
440
704
|
}
|
|
441
|
-
|
|
442
|
-
// Report unhandled errors
|
|
443
|
-
callOnError(error, "routing", {
|
|
444
|
-
request,
|
|
445
|
-
url,
|
|
446
|
-
env,
|
|
447
|
-
handledByBoundary: false,
|
|
448
|
-
});
|
|
449
|
-
console.error(`[RSC] Error:`, error);
|
|
450
|
-
throw error;
|
|
451
705
|
}
|
|
706
|
+
|
|
707
|
+
// ---- 4. Execute ----
|
|
708
|
+
return executeRequest(
|
|
709
|
+
plan as ExecutableRequestPlan<TEnv>,
|
|
710
|
+
request,
|
|
711
|
+
env,
|
|
712
|
+
url,
|
|
713
|
+
variables,
|
|
714
|
+
nonce,
|
|
715
|
+
);
|
|
452
716
|
}
|
|
453
717
|
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
async function handleProgressiveEnhancement(
|
|
718
|
+
// Execute a classified request plan. Dispatches to the appropriate handler
|
|
719
|
+
// based on plan.mode. Lives in the createRSCHandler closure for access to
|
|
720
|
+
// handlerCtx, router, callOnError, etc.
|
|
721
|
+
// Only receives executable plans (version-mismatch is handled above).
|
|
722
|
+
async function executeRequest(
|
|
723
|
+
plan: ExecutableRequestPlan<TEnv>,
|
|
461
724
|
request: Request,
|
|
462
725
|
env: TEnv,
|
|
463
726
|
url: URL,
|
|
464
|
-
|
|
465
|
-
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
727
|
+
variables: Record<string, any>,
|
|
466
728
|
nonce: string | undefined,
|
|
467
|
-
): Promise<Response
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
contentType.includes("multipart/form-data") ||
|
|
471
|
-
contentType.includes("application/x-www-form-urlencoded");
|
|
472
|
-
|
|
473
|
-
if (request.method !== "POST" || isAction || !isFormSubmission) {
|
|
474
|
-
return null;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Clone the request to read FormData without consuming it
|
|
478
|
-
const formData = await request.clone().formData();
|
|
479
|
-
|
|
480
|
-
// Look for React's progressive enhancement hidden fields
|
|
481
|
-
let isDirectAction = false;
|
|
482
|
-
let isUseActionState = false;
|
|
483
|
-
let directActionId: string | null = null;
|
|
729
|
+
): Promise<Response> {
|
|
730
|
+
// Common setup
|
|
731
|
+
const handleStore = requireRequestContext()._handleStore;
|
|
484
732
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
733
|
+
// Wire up error reporting for late streaming-handle failures
|
|
734
|
+
handleStore.onError = (error: Error) => {
|
|
735
|
+
const reqCtx = requireRequestContext();
|
|
736
|
+
callOnError(error, "handler", {
|
|
737
|
+
request,
|
|
738
|
+
url,
|
|
739
|
+
routeKey: reqCtx._routeName,
|
|
740
|
+
params: reqCtx.params as Record<string, string>,
|
|
741
|
+
handledByBoundary: true,
|
|
742
|
+
});
|
|
743
|
+
try {
|
|
744
|
+
const routerCtx = getRouterContext();
|
|
745
|
+
if (routerCtx?.telemetry) {
|
|
746
|
+
safeEmit(resolveSink(routerCtx.telemetry), {
|
|
747
|
+
type: "handler.error" as const,
|
|
748
|
+
timestamp: performance.now(),
|
|
749
|
+
requestId: routerCtx.requestId,
|
|
750
|
+
error,
|
|
751
|
+
handledByBoundary: true,
|
|
752
|
+
pathname: url.pathname,
|
|
753
|
+
routeKey: reqCtx._routeName,
|
|
754
|
+
params: reqCtx.params as Record<string, string>,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
} catch {
|
|
758
|
+
// Router context may not be available (e.g. prerender path)
|
|
491
759
|
}
|
|
492
|
-
}
|
|
760
|
+
};
|
|
493
761
|
|
|
494
|
-
|
|
495
|
-
|
|
762
|
+
// Set route params early so all execution paths can access ctx.params.
|
|
763
|
+
// Also store the classified snapshot so match/matchPartial can reuse it
|
|
764
|
+
// instead of calling resolveRoute again.
|
|
765
|
+
if (plan.mode !== "redirect") {
|
|
766
|
+
setRequestContextParams(plan.route.params, plan.route.routeKey);
|
|
767
|
+
requireRequestContext()._classifiedRoute = plan.route;
|
|
496
768
|
}
|
|
497
769
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
770
|
+
const routeReverse = createReverseFunction(getRequiredRouteMap());
|
|
771
|
+
|
|
772
|
+
// ---- Response route: skip entire RSC pipeline ----
|
|
773
|
+
if (plan.mode === "response") {
|
|
774
|
+
// Build ResponseRouteMatch from plan fields. handleResponseRoute
|
|
775
|
+
// expects a flat object with params at the top level.
|
|
776
|
+
const responseMatch: ResponseRouteMatch = {
|
|
777
|
+
responseType: plan.responseType,
|
|
778
|
+
handler: plan.handler,
|
|
779
|
+
params: plan.route.params,
|
|
780
|
+
negotiated: plan.negotiated,
|
|
781
|
+
manifestEntry: plan.manifestEntry,
|
|
782
|
+
routeMiddleware: plan.routeMiddleware,
|
|
783
|
+
};
|
|
784
|
+
const responseOutcome = await withTimeout(
|
|
785
|
+
handleResponseRoute(
|
|
786
|
+
handlerCtx,
|
|
787
|
+
responseMatch,
|
|
508
788
|
request,
|
|
789
|
+
env,
|
|
509
790
|
url,
|
|
791
|
+
variables,
|
|
792
|
+
),
|
|
793
|
+
router.timeouts.renderStartMs,
|
|
794
|
+
"render-start",
|
|
795
|
+
);
|
|
796
|
+
if (responseOutcome.timedOut) {
|
|
797
|
+
return handleTimeoutResponse(
|
|
798
|
+
request,
|
|
510
799
|
env,
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
800
|
+
url,
|
|
801
|
+
"render-start",
|
|
802
|
+
responseOutcome.durationMs,
|
|
803
|
+
plan.route.routeKey,
|
|
804
|
+
);
|
|
514
805
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
let args: unknown[] = [];
|
|
519
|
-
try {
|
|
520
|
-
args = await decodeReply(formData, { temporaryReferences });
|
|
521
|
-
} catch {
|
|
522
|
-
args = [formData];
|
|
806
|
+
const response = responseOutcome.result;
|
|
807
|
+
if (plan.negotiated) {
|
|
808
|
+
response.headers.append("Vary", "Accept");
|
|
523
809
|
}
|
|
810
|
+
return response;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// SSR setup: kick off in parallel for modes that need HTML rendering.
|
|
814
|
+
// Placed after response-route short-circuit so response/mime routes
|
|
815
|
+
// never pay for SSR work.
|
|
816
|
+
if (plan.mode !== "loader" && mayNeedSSR(request, url)) {
|
|
817
|
+
variables[SSR_SETUP_VAR] = startSSRSetup(
|
|
818
|
+
handlerCtx,
|
|
819
|
+
request,
|
|
820
|
+
env,
|
|
821
|
+
url,
|
|
822
|
+
router.debugPerformance
|
|
823
|
+
? () => requireRequestContext()._metricsStore
|
|
824
|
+
: undefined,
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// ---- Loader fetch ----
|
|
829
|
+
if (plan.mode === "loader") {
|
|
830
|
+
return handleLoaderFetch(
|
|
831
|
+
handlerCtx,
|
|
832
|
+
request,
|
|
833
|
+
env,
|
|
834
|
+
url,
|
|
835
|
+
variables,
|
|
836
|
+
plan.route.params,
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// ---- Progressive enhancement ----
|
|
841
|
+
if (plan.mode === "pe-render") {
|
|
842
|
+
const peResult = await handleProgressiveEnhancement(
|
|
843
|
+
handlerCtx,
|
|
844
|
+
request,
|
|
845
|
+
env,
|
|
846
|
+
url,
|
|
847
|
+
false, // isAction = false for PE
|
|
848
|
+
handleStore,
|
|
849
|
+
nonce,
|
|
850
|
+
{
|
|
851
|
+
routeMiddleware: plan.route.routeMiddleware,
|
|
852
|
+
variables,
|
|
853
|
+
routeReverse,
|
|
854
|
+
},
|
|
855
|
+
);
|
|
856
|
+
if (peResult) return peResult;
|
|
857
|
+
// PE handler returned null (not a PE form) — fall through to render
|
|
858
|
+
}
|
|
524
859
|
|
|
860
|
+
// ---- Action: execute action, then revalidate wrapped in route middleware ----
|
|
861
|
+
if (plan.mode === "action") {
|
|
862
|
+
let actionContinuation: ActionContinuation | undefined;
|
|
525
863
|
try {
|
|
526
|
-
const
|
|
527
|
-
|
|
864
|
+
const actionOutcome = await withTimeout(
|
|
865
|
+
executeServerAction(
|
|
866
|
+
handlerCtx,
|
|
867
|
+
request,
|
|
868
|
+
env,
|
|
869
|
+
url,
|
|
870
|
+
plan.actionId,
|
|
871
|
+
handleStore,
|
|
872
|
+
),
|
|
873
|
+
router.timeouts.actionMs,
|
|
874
|
+
"action",
|
|
875
|
+
);
|
|
876
|
+
if (actionOutcome.timedOut) {
|
|
877
|
+
return handleTimeoutResponse(
|
|
878
|
+
request,
|
|
879
|
+
env,
|
|
880
|
+
url,
|
|
881
|
+
"action",
|
|
882
|
+
actionOutcome.durationMs,
|
|
883
|
+
plan.route.routeKey,
|
|
884
|
+
plan.actionId,
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
const result = actionOutcome.result;
|
|
888
|
+
// Response means redirect or error boundary — done.
|
|
889
|
+
if (result instanceof Response) return result;
|
|
890
|
+
actionContinuation = result;
|
|
528
891
|
} catch (error) {
|
|
529
892
|
callOnError(error, "action", {
|
|
530
893
|
request,
|
|
531
894
|
url,
|
|
532
895
|
env,
|
|
533
|
-
actionId:
|
|
896
|
+
actionId: plan.actionId,
|
|
534
897
|
handledByBoundary: false,
|
|
535
898
|
});
|
|
536
|
-
console.error(
|
|
899
|
+
console.error(`[RSC] Action error:`, error);
|
|
900
|
+
throw error;
|
|
537
901
|
}
|
|
538
|
-
}
|
|
539
902
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
903
|
+
// Revalidation render wrapped in route middleware.
|
|
904
|
+
// Actions from client-side navigation include _rsc_partial — preserve
|
|
905
|
+
// the partial flag so the revalidation returns a Flight stream, not HTML.
|
|
906
|
+
// App-switch is already excluded by classifyRequest (would be full-render).
|
|
907
|
+
const isPartialAction = url.searchParams.has("_rsc_partial");
|
|
908
|
+
return executeRenderWithMiddleware(
|
|
909
|
+
plan.route.routeMiddleware,
|
|
910
|
+
plan.negotiated,
|
|
911
|
+
plan.route.routeKey,
|
|
912
|
+
routeReverse,
|
|
545
913
|
request,
|
|
546
|
-
url,
|
|
547
914
|
env,
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
method: "GET",
|
|
556
|
-
headers: new Headers({ accept: "text/html" }),
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
const match = await router.match(renderRequest, env);
|
|
560
|
-
|
|
561
|
-
if (match.redirect) {
|
|
562
|
-
return new Response(null, {
|
|
563
|
-
status: 308,
|
|
564
|
-
headers: { Location: match.redirect },
|
|
565
|
-
});
|
|
915
|
+
url,
|
|
916
|
+
variables,
|
|
917
|
+
nonce,
|
|
918
|
+
handleStore,
|
|
919
|
+
isPartialAction,
|
|
920
|
+
actionContinuation,
|
|
921
|
+
);
|
|
566
922
|
}
|
|
567
923
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
segments: match.segments,
|
|
577
|
-
matched: match.matched,
|
|
578
|
-
diff: match.diff,
|
|
579
|
-
isPartial: false,
|
|
580
|
-
rootLayout: router.rootLayout,
|
|
581
|
-
handles: handleStore.stream(),
|
|
582
|
-
version,
|
|
583
|
-
themeConfig: router.themeConfig,
|
|
584
|
-
warmupEnabled: router.warmupEnabled,
|
|
585
|
-
initialTheme: requireRequestContext().theme,
|
|
586
|
-
},
|
|
587
|
-
formState: actionResult,
|
|
588
|
-
};
|
|
589
|
-
|
|
590
|
-
const rscStream = renderToReadableStream<RscPayload>(payload);
|
|
591
|
-
const ssrModule = await loadSSRModule();
|
|
592
|
-
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
593
|
-
formState: reactFormState,
|
|
594
|
-
nonce,
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
return new Response(htmlStream, {
|
|
598
|
-
headers: { "content-type": "text/html;charset=utf-8" },
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// ============================================================================
|
|
603
|
-
// SERVER ACTION HANDLER
|
|
604
|
-
// ============================================================================
|
|
605
|
-
async function handleServerAction(
|
|
606
|
-
request: Request,
|
|
607
|
-
env: TEnv,
|
|
608
|
-
url: URL,
|
|
609
|
-
actionId: string,
|
|
610
|
-
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
611
|
-
): Promise<Response> {
|
|
612
|
-
const temporaryReferences = createTemporaryReferenceSet();
|
|
613
|
-
|
|
614
|
-
// Decode action arguments from request body
|
|
615
|
-
const contentType = request.headers.get("content-type") || "";
|
|
616
|
-
let args: unknown[] = [];
|
|
617
|
-
let actionFormData: FormData | undefined;
|
|
618
|
-
|
|
619
|
-
try {
|
|
620
|
-
const body = contentType.includes("multipart/form-data")
|
|
621
|
-
? await request.formData()
|
|
622
|
-
: await request.text();
|
|
623
|
-
|
|
624
|
-
if (body instanceof FormData) {
|
|
625
|
-
actionFormData = body;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
if (hasBodyContent(body)) {
|
|
629
|
-
args = await decodeReply(body, { temporaryReferences });
|
|
630
|
-
}
|
|
631
|
-
} catch (error) {
|
|
632
|
-
callOnError(error, "action", {
|
|
924
|
+
// ---- Full render / Partial render (or PE that fell through) ----
|
|
925
|
+
if (plan.mode === "full-render" || plan.mode === "partial-render") {
|
|
926
|
+
const isPartial = plan.mode === "partial-render";
|
|
927
|
+
return executeRenderWithMiddleware(
|
|
928
|
+
plan.route.routeMiddleware,
|
|
929
|
+
plan.negotiated,
|
|
930
|
+
plan.route.routeKey,
|
|
931
|
+
routeReverse,
|
|
633
932
|
request,
|
|
634
|
-
url,
|
|
635
933
|
env,
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
934
|
+
url,
|
|
935
|
+
variables,
|
|
936
|
+
nonce,
|
|
937
|
+
handleStore,
|
|
938
|
+
isPartial,
|
|
939
|
+
);
|
|
642
940
|
}
|
|
643
941
|
|
|
644
|
-
//
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
returnValue = { ok: true, data };
|
|
653
|
-
} catch (error) {
|
|
654
|
-
returnValue = { ok: false, data: error };
|
|
655
|
-
actionStatus = 500;
|
|
656
|
-
|
|
657
|
-
// Try to render error boundary
|
|
658
|
-
const errorResult = await router.matchError(request, env, error, "route");
|
|
659
|
-
|
|
660
|
-
// Report the action error (handledByBoundary indicates if error boundary will render)
|
|
661
|
-
callOnError(error, "action", {
|
|
942
|
+
// PE that fell through (handleProgressiveEnhancement returned null)
|
|
943
|
+
// falls back to full render
|
|
944
|
+
if (plan.mode === "pe-render") {
|
|
945
|
+
return executeRenderWithMiddleware(
|
|
946
|
+
plan.route.routeMiddleware,
|
|
947
|
+
false,
|
|
948
|
+
plan.route.routeKey,
|
|
949
|
+
routeReverse,
|
|
662
950
|
request,
|
|
663
|
-
url,
|
|
664
951
|
env,
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const payload: RscPayload = {
|
|
673
|
-
root: null,
|
|
674
|
-
metadata: {
|
|
675
|
-
pathname: url.pathname,
|
|
676
|
-
segments: errorResult.segments,
|
|
677
|
-
isPartial: true,
|
|
678
|
-
matched: errorResult.matched,
|
|
679
|
-
diff: errorResult.diff,
|
|
680
|
-
isError: true,
|
|
681
|
-
handles: handleStore.stream(),
|
|
682
|
-
version,
|
|
683
|
-
},
|
|
684
|
-
returnValue,
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
const rscStream = renderToReadableStream<RscPayload>(payload, {
|
|
688
|
-
temporaryReferences,
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
return createResponseWithMergedHeaders(rscStream, {
|
|
692
|
-
status: actionStatus,
|
|
693
|
-
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// Revalidate after action
|
|
699
|
-
const resolvedActionId =
|
|
700
|
-
(loadedAction as { $id?: string; $$id?: string } | undefined)?.$id ??
|
|
701
|
-
(loadedAction as { $$id?: string } | undefined)?.$$id ??
|
|
702
|
-
actionId;
|
|
703
|
-
const actionContext = {
|
|
704
|
-
actionId: resolvedActionId,
|
|
705
|
-
actionUrl: new URL(request.url),
|
|
706
|
-
actionResult: returnValue.data,
|
|
707
|
-
formData: actionFormData,
|
|
708
|
-
};
|
|
709
|
-
|
|
710
|
-
const matchResult = await router.matchPartial(request, env, actionContext);
|
|
711
|
-
|
|
712
|
-
if (!matchResult) {
|
|
713
|
-
// Fall back to full render
|
|
714
|
-
const fullMatch = await router.match(request, env);
|
|
715
|
-
setRequestContextParams(fullMatch.params);
|
|
716
|
-
|
|
717
|
-
if (fullMatch.redirect) {
|
|
718
|
-
return createResponseWithMergedHeaders(null, {
|
|
719
|
-
status: 308,
|
|
720
|
-
headers: { Location: fullMatch.redirect },
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const renderStart = performance.now();
|
|
725
|
-
const root = renderSegments(fullMatch.segments, {
|
|
726
|
-
rootLayout: router.rootLayout,
|
|
727
|
-
isAction: true,
|
|
728
|
-
});
|
|
729
|
-
const renderDuration = performance.now() - renderStart;
|
|
730
|
-
const serverTiming = fullMatch.serverTiming
|
|
731
|
-
? `${fullMatch.serverTiming}, rendering;dur=${renderDuration.toFixed(2)}`
|
|
732
|
-
: `rendering;dur=${renderDuration.toFixed(2)}`;
|
|
733
|
-
|
|
734
|
-
const payload: RscPayload = {
|
|
735
|
-
root,
|
|
736
|
-
metadata: {
|
|
737
|
-
pathname: url.pathname,
|
|
738
|
-
segments: fullMatch.segments,
|
|
739
|
-
matched: fullMatch.matched,
|
|
740
|
-
diff: fullMatch.diff,
|
|
741
|
-
handles: handleStore.stream(),
|
|
742
|
-
version,
|
|
743
|
-
},
|
|
744
|
-
returnValue,
|
|
745
|
-
};
|
|
746
|
-
|
|
747
|
-
const rscStream = renderToReadableStream<RscPayload>(payload, {
|
|
748
|
-
temporaryReferences,
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
const headers: Record<string, string> = {
|
|
752
|
-
"content-type": "text/x-component;charset=utf-8",
|
|
753
|
-
};
|
|
754
|
-
if (serverTiming) {
|
|
755
|
-
headers["Server-Timing"] = serverTiming;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
return createResponseWithMergedHeaders(rscStream, {
|
|
759
|
-
status: actionStatus,
|
|
760
|
-
headers,
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// Return updated segments
|
|
765
|
-
setRequestContextParams(matchResult.params);
|
|
766
|
-
|
|
767
|
-
const renderStart = performance.now();
|
|
768
|
-
|
|
769
|
-
const renderDuration = performance.now() - renderStart;
|
|
770
|
-
const serverTiming = matchResult.serverTiming
|
|
771
|
-
? `${matchResult.serverTiming}, rendering;dur=${renderDuration.toFixed(2)}`
|
|
772
|
-
: `rendering;dur=${renderDuration.toFixed(2)}`;
|
|
773
|
-
|
|
774
|
-
const payload: RscPayload = {
|
|
775
|
-
root: null,
|
|
776
|
-
metadata: {
|
|
777
|
-
pathname: url.pathname,
|
|
778
|
-
segments: matchResult.segments,
|
|
779
|
-
isPartial: true,
|
|
780
|
-
matched: matchResult.matched,
|
|
781
|
-
diff: matchResult.diff,
|
|
782
|
-
slots: matchResult.slots,
|
|
783
|
-
handles: handleStore.stream(),
|
|
784
|
-
version,
|
|
785
|
-
},
|
|
786
|
-
returnValue,
|
|
787
|
-
};
|
|
788
|
-
|
|
789
|
-
const rscStream = renderToReadableStream<RscPayload>(payload, {
|
|
790
|
-
temporaryReferences,
|
|
791
|
-
});
|
|
792
|
-
|
|
793
|
-
const actionHeaders: Record<string, string> = {
|
|
794
|
-
"content-type": "text/x-component;charset=utf-8",
|
|
795
|
-
};
|
|
796
|
-
if (serverTiming) {
|
|
797
|
-
actionHeaders["Server-Timing"] = serverTiming;
|
|
952
|
+
url,
|
|
953
|
+
variables,
|
|
954
|
+
nonce,
|
|
955
|
+
handleStore,
|
|
956
|
+
false,
|
|
957
|
+
);
|
|
798
958
|
}
|
|
799
959
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
960
|
+
// Redirect plan that wasn't handled above (full-page redirect — let
|
|
961
|
+
// the pipeline handle it via match() which returns { redirect: url })
|
|
962
|
+
return executeRenderWithMiddleware(
|
|
963
|
+
plan.route.routeMiddleware,
|
|
964
|
+
false,
|
|
965
|
+
plan.route.routeKey,
|
|
966
|
+
routeReverse,
|
|
967
|
+
request,
|
|
968
|
+
env,
|
|
969
|
+
url,
|
|
970
|
+
variables,
|
|
971
|
+
nonce,
|
|
972
|
+
handleStore,
|
|
973
|
+
false,
|
|
974
|
+
);
|
|
804
975
|
}
|
|
805
976
|
|
|
806
|
-
//
|
|
807
|
-
//
|
|
808
|
-
//
|
|
809
|
-
|
|
810
|
-
|
|
977
|
+
// Shared render execution: wraps handleRscRendering (or revalidateAfterAction)
|
|
978
|
+
// in route middleware and timeout handling. Consolidates the pattern used by
|
|
979
|
+
// action-revalidate, full-render, and partial-render modes.
|
|
980
|
+
async function executeRenderWithMiddleware(
|
|
981
|
+
routeMiddleware: import("../router/middleware-types.js").CollectedMiddleware[],
|
|
982
|
+
negotiated: boolean,
|
|
983
|
+
routeKey: string,
|
|
984
|
+
routeReverse: ReturnType<typeof createReverseFunction>,
|
|
811
985
|
request: Request,
|
|
812
986
|
env: TEnv,
|
|
813
987
|
url: URL,
|
|
814
988
|
variables: Record<string, any>,
|
|
989
|
+
nonce: string | undefined,
|
|
990
|
+
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
991
|
+
isPartial: boolean,
|
|
992
|
+
actionContinuation?: ActionContinuation,
|
|
815
993
|
): Promise<Response> {
|
|
816
|
-
const
|
|
994
|
+
const renderHandler = async (): Promise<Response> => {
|
|
995
|
+
try {
|
|
996
|
+
let response: Response;
|
|
997
|
+
if (actionContinuation) {
|
|
998
|
+
response = await revalidateAfterAction(
|
|
999
|
+
handlerCtx,
|
|
1000
|
+
request,
|
|
1001
|
+
env,
|
|
1002
|
+
url,
|
|
1003
|
+
handleStore,
|
|
1004
|
+
actionContinuation,
|
|
1005
|
+
);
|
|
1006
|
+
} else {
|
|
1007
|
+
response = await handleRscRendering(
|
|
1008
|
+
handlerCtx,
|
|
1009
|
+
request,
|
|
1010
|
+
env,
|
|
1011
|
+
url,
|
|
1012
|
+
isPartial,
|
|
1013
|
+
handleStore,
|
|
1014
|
+
nonce,
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
if (negotiated) {
|
|
1018
|
+
response.headers.append("Vary", "Accept");
|
|
1019
|
+
}
|
|
1020
|
+
return response;
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
// Check if middleware/handler returned Response
|
|
1023
|
+
if (error instanceof Response) {
|
|
1024
|
+
// During partial (client-side navigation), a 200 Response from a handler
|
|
1025
|
+
// means the route serves raw content (JSON, text, etc.), not JSX.
|
|
1026
|
+
// Signal the browser to hard-navigate so it renders the raw response.
|
|
1027
|
+
if (isPartial && error.status === 200) {
|
|
1028
|
+
console.warn(
|
|
1029
|
+
`[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
|
|
1030
|
+
`Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
|
|
1031
|
+
);
|
|
1032
|
+
return createResponseWithMergedHeaders(null, {
|
|
1033
|
+
status: 200,
|
|
1034
|
+
headers: {
|
|
1035
|
+
"X-RSC-Reload": stripInternalParams(url).toString(),
|
|
1036
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
1037
|
+
},
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
817
1040
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1041
|
+
if (isPartial) {
|
|
1042
|
+
const intercepted = interceptRedirectForPartial(
|
|
1043
|
+
error,
|
|
1044
|
+
createRedirectFlightResponse,
|
|
1045
|
+
);
|
|
1046
|
+
if (intercepted) return intercepted;
|
|
1047
|
+
}
|
|
823
1048
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
if (!registeredLoader) {
|
|
827
|
-
return createResponseWithMergedHeaders(
|
|
828
|
-
`Loader "${loaderId}" not found in registry`,
|
|
829
|
-
{ status: 404 },
|
|
830
|
-
);
|
|
831
|
-
}
|
|
1049
|
+
return error;
|
|
1050
|
+
}
|
|
832
1051
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1052
|
+
// Render 404 page for unmatched routes
|
|
1053
|
+
const isRouteNotFound =
|
|
1054
|
+
error instanceof RouteNotFoundError ||
|
|
1055
|
+
(error instanceof Error && error.name === "RouteNotFoundError");
|
|
1056
|
+
if (isRouteNotFound) {
|
|
1057
|
+
callOnError(error, "routing", {
|
|
1058
|
+
request,
|
|
1059
|
+
url,
|
|
1060
|
+
env,
|
|
1061
|
+
handledByBoundary: true,
|
|
1062
|
+
});
|
|
837
1063
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1064
|
+
const notFoundOption = router.notFound;
|
|
1065
|
+
const notFoundComponent =
|
|
1066
|
+
typeof notFoundOption === "function"
|
|
1067
|
+
? notFoundOption({ pathname: url.pathname })
|
|
1068
|
+
: (notFoundOption ?? createElement("h1", null, "Not Found"));
|
|
1069
|
+
|
|
1070
|
+
const notFoundSegment = {
|
|
1071
|
+
id: "notFound",
|
|
1072
|
+
namespace: "notFound",
|
|
1073
|
+
type: "route" as const,
|
|
1074
|
+
index: 0,
|
|
1075
|
+
component: notFoundComponent,
|
|
1076
|
+
params: {},
|
|
845
1077
|
};
|
|
846
|
-
loaderParams = jsonBody.params ?? {};
|
|
847
|
-
loaderBody = jsonBody.body;
|
|
848
|
-
}
|
|
849
|
-
} catch {
|
|
850
|
-
return createResponseWithMergedHeaders("Invalid JSON body", {
|
|
851
|
-
status: 400,
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
} else {
|
|
855
|
-
const loaderParamsJson = url.searchParams.get("_rsc_loader_params");
|
|
856
|
-
if (loaderParamsJson) {
|
|
857
|
-
try {
|
|
858
|
-
loaderParams = JSON.parse(loaderParamsJson);
|
|
859
|
-
} catch {
|
|
860
|
-
return createResponseWithMergedHeaders(
|
|
861
|
-
"Invalid _rsc_loader_params JSON",
|
|
862
|
-
{ status: 400 },
|
|
863
|
-
);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
// Execute the loader with middleware
|
|
869
|
-
try {
|
|
870
|
-
const { fn, middleware } = registeredLoader;
|
|
871
1078
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1079
|
+
const payload: RscPayload = {
|
|
1080
|
+
metadata: {
|
|
1081
|
+
pathname: url.pathname,
|
|
1082
|
+
routerId: router.id,
|
|
1083
|
+
basename: router.basename,
|
|
1084
|
+
segments: [notFoundSegment],
|
|
1085
|
+
matched: [],
|
|
1086
|
+
diff: [],
|
|
1087
|
+
isPartial: false,
|
|
1088
|
+
rootLayout: router.rootLayout,
|
|
1089
|
+
handles: handleStore.stream(),
|
|
1090
|
+
version,
|
|
1091
|
+
themeConfig: router.themeConfig,
|
|
1092
|
+
warmupEnabled: router.warmupEnabled,
|
|
1093
|
+
initialTheme: requireRequestContext().theme,
|
|
1094
|
+
},
|
|
884
1095
|
};
|
|
885
1096
|
|
|
886
|
-
const
|
|
1097
|
+
const rscStream = renderToReadableStream(payload, {
|
|
1098
|
+
onError: (error: unknown) => {
|
|
1099
|
+
callOnError(error, "rendering", { request, url, env });
|
|
1100
|
+
},
|
|
1101
|
+
});
|
|
887
1102
|
|
|
888
|
-
|
|
889
|
-
|
|
1103
|
+
const isRscRequest =
|
|
1104
|
+
isPartial ||
|
|
1105
|
+
(!request.headers.get("accept")?.includes("text/html") &&
|
|
1106
|
+
!url.searchParams.has("__html")) ||
|
|
1107
|
+
url.searchParams.has("__rsc");
|
|
1108
|
+
|
|
1109
|
+
if (isRscRequest) {
|
|
1110
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
1111
|
+
status: 404,
|
|
1112
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
1113
|
+
});
|
|
890
1114
|
}
|
|
891
|
-
const loaderPayload: LoaderPayload = { loaderResult: result };
|
|
892
|
-
const rscStream =
|
|
893
|
-
renderToReadableStream<LoaderPayload>(loaderPayload);
|
|
894
1115
|
|
|
895
|
-
|
|
896
|
-
|
|
1116
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
1117
|
+
handlerCtx,
|
|
1118
|
+
request,
|
|
1119
|
+
env,
|
|
1120
|
+
url,
|
|
1121
|
+
requireRequestContext()._metricsStore,
|
|
1122
|
+
);
|
|
1123
|
+
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
1124
|
+
nonce,
|
|
1125
|
+
streamMode,
|
|
897
1126
|
});
|
|
898
|
-
},
|
|
899
|
-
);
|
|
900
|
-
} catch (error) {
|
|
901
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
902
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
903
|
-
|
|
904
|
-
console.error("[RSC] Loader error:", error);
|
|
905
|
-
|
|
906
|
-
callOnError(error, "loader", {
|
|
907
|
-
request,
|
|
908
|
-
url,
|
|
909
|
-
env,
|
|
910
|
-
loaderName: loaderId,
|
|
911
|
-
handledByBoundary: false,
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
const errorPayload = {
|
|
915
|
-
loaderResult: null,
|
|
916
|
-
loaderError: {
|
|
917
|
-
message: isDev ? err.message : "An error occurred",
|
|
918
|
-
name: err.name,
|
|
919
|
-
},
|
|
920
|
-
};
|
|
921
|
-
const rscStream = renderToReadableStream(errorPayload);
|
|
922
1127
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// ============================================================================
|
|
931
|
-
// RSC RENDERING HANDLER (Navigation)
|
|
932
|
-
// ============================================================================
|
|
933
|
-
async function handleRscRendering(
|
|
934
|
-
request: Request,
|
|
935
|
-
env: TEnv,
|
|
936
|
-
url: URL,
|
|
937
|
-
isPartial: boolean,
|
|
938
|
-
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
939
|
-
nonce: string | undefined,
|
|
940
|
-
): Promise<Response> {
|
|
941
|
-
let payload: RscPayload;
|
|
942
|
-
let serverTiming: string | undefined;
|
|
943
|
-
|
|
944
|
-
if (isPartial) {
|
|
945
|
-
// Partial render (navigation)
|
|
946
|
-
const result = await router.matchPartial(request, env);
|
|
947
|
-
|
|
948
|
-
if (!result) {
|
|
949
|
-
// Fall back to full render
|
|
950
|
-
const match = await router.match(request, env);
|
|
951
|
-
setRequestContextParams(match.params);
|
|
952
|
-
|
|
953
|
-
if (match.redirect) {
|
|
954
|
-
return createResponseWithMergedHeaders(null, {
|
|
955
|
-
status: 308,
|
|
956
|
-
headers: { Location: match.redirect },
|
|
1128
|
+
return createResponseWithMergedHeaders(htmlStream, {
|
|
1129
|
+
status: 404,
|
|
1130
|
+
headers: { "content-type": "text/html;charset=utf-8" },
|
|
957
1131
|
});
|
|
958
1132
|
}
|
|
959
1133
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
? `${match.serverTiming}, rendering;dur=${renderDuration.toFixed(2)}`
|
|
967
|
-
: `rendering;dur=${renderDuration.toFixed(2)}`;
|
|
968
|
-
|
|
969
|
-
payload = {
|
|
970
|
-
root,
|
|
971
|
-
metadata: {
|
|
972
|
-
pathname: url.pathname,
|
|
973
|
-
segments: match.segments,
|
|
974
|
-
matched: match.matched,
|
|
975
|
-
diff: match.diff,
|
|
976
|
-
isPartial: false,
|
|
977
|
-
handles: handleStore.stream(),
|
|
978
|
-
version,
|
|
979
|
-
themeConfig: router.themeConfig,
|
|
980
|
-
initialTheme: requireRequestContext().theme,
|
|
981
|
-
},
|
|
982
|
-
};
|
|
983
|
-
} else {
|
|
984
|
-
setRequestContextParams(result.params);
|
|
985
|
-
serverTiming = result.serverTiming;
|
|
986
|
-
|
|
987
|
-
payload = {
|
|
988
|
-
root: null,
|
|
989
|
-
metadata: {
|
|
990
|
-
pathname: url.pathname,
|
|
991
|
-
segments: result.segments,
|
|
992
|
-
matched: result.matched,
|
|
993
|
-
diff: result.diff,
|
|
994
|
-
isPartial: true,
|
|
995
|
-
slots: result.slots,
|
|
996
|
-
handles: handleStore.stream(),
|
|
997
|
-
version,
|
|
998
|
-
},
|
|
999
|
-
};
|
|
1000
|
-
}
|
|
1001
|
-
} else {
|
|
1002
|
-
// Full render (initial page load)
|
|
1003
|
-
const match = await router.match(request, env);
|
|
1004
|
-
setRequestContextParams(match.params);
|
|
1005
|
-
|
|
1006
|
-
if (match.redirect) {
|
|
1007
|
-
return createResponseWithMergedHeaders(null, {
|
|
1008
|
-
status: 308,
|
|
1009
|
-
headers: { Location: match.redirect },
|
|
1134
|
+
// Report unhandled errors
|
|
1135
|
+
callOnError(error, "routing", {
|
|
1136
|
+
request,
|
|
1137
|
+
url,
|
|
1138
|
+
env,
|
|
1139
|
+
handledByBoundary: false,
|
|
1010
1140
|
});
|
|
1141
|
+
console.error(`[RSC] Error:`, error);
|
|
1142
|
+
throw error;
|
|
1011
1143
|
}
|
|
1144
|
+
};
|
|
1012
1145
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
payload = {
|
|
1026
|
-
root,
|
|
1027
|
-
metadata: {
|
|
1028
|
-
pathname: url.pathname,
|
|
1029
|
-
segments: match.segments,
|
|
1030
|
-
matched: match.matched,
|
|
1031
|
-
diff: match.diff,
|
|
1032
|
-
isPartial: false,
|
|
1033
|
-
rootLayout: router.rootLayout,
|
|
1034
|
-
handles: handleStore.stream(),
|
|
1035
|
-
version,
|
|
1036
|
-
themeConfig: router.themeConfig,
|
|
1037
|
-
initialTheme: requireRequestContext().theme,
|
|
1038
|
-
},
|
|
1039
|
-
};
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
// Serialize to RSC stream
|
|
1043
|
-
const rscStream = renderToReadableStream<RscPayload>(payload);
|
|
1146
|
+
// Wrap the render path in a renderStartMs timeout
|
|
1147
|
+
const executeRender = async (): Promise<Response> => {
|
|
1148
|
+
if (routeMiddleware.length > 0) {
|
|
1149
|
+
const mwResponse = await executeMiddleware(
|
|
1150
|
+
buildRouteMiddlewareEntries<TEnv>(routeMiddleware),
|
|
1151
|
+
request,
|
|
1152
|
+
env,
|
|
1153
|
+
variables,
|
|
1154
|
+
renderHandler,
|
|
1155
|
+
routeReverse,
|
|
1156
|
+
);
|
|
1044
1157
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1158
|
+
if (isPartial || actionContinuation) {
|
|
1159
|
+
const intercepted = interceptRedirectForPartial(
|
|
1160
|
+
mwResponse,
|
|
1161
|
+
createRedirectFlightResponse,
|
|
1162
|
+
);
|
|
1163
|
+
if (intercepted) return intercepted;
|
|
1164
|
+
}
|
|
1050
1165
|
|
|
1051
|
-
|
|
1052
|
-
const rscHeaders: Record<string, string> = {
|
|
1053
|
-
"content-type": "text/x-component;charset=utf-8",
|
|
1054
|
-
vary: "accept",
|
|
1055
|
-
};
|
|
1056
|
-
if (serverTiming) {
|
|
1057
|
-
rscHeaders["Server-Timing"] = serverTiming;
|
|
1166
|
+
return finalizeResponse(mwResponse);
|
|
1058
1167
|
}
|
|
1059
|
-
return createResponseWithMergedHeaders(rscStream, {
|
|
1060
|
-
headers: rscHeaders,
|
|
1061
|
-
});
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
// Delegate to SSR for HTML response
|
|
1065
|
-
const ssrModule = await loadSSRModule();
|
|
1066
|
-
const htmlStream = await ssrModule.renderHTML(rscStream, { nonce });
|
|
1067
1168
|
|
|
1068
|
-
|
|
1069
|
-
"content-type": "text/html;charset=utf-8",
|
|
1169
|
+
return renderHandler();
|
|
1070
1170
|
};
|
|
1071
|
-
if (serverTiming) {
|
|
1072
|
-
htmlHeaders["Server-Timing"] = serverTiming;
|
|
1073
|
-
}
|
|
1074
1171
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1172
|
+
const renderOutcome = await withTimeout(
|
|
1173
|
+
executeRender(),
|
|
1174
|
+
router.timeouts.renderStartMs,
|
|
1175
|
+
"render-start",
|
|
1176
|
+
);
|
|
1177
|
+
if (renderOutcome.timedOut) {
|
|
1178
|
+
return handleTimeoutResponse(
|
|
1179
|
+
request,
|
|
1180
|
+
env,
|
|
1181
|
+
url,
|
|
1182
|
+
"render-start",
|
|
1183
|
+
renderOutcome.durationMs,
|
|
1184
|
+
routeKey,
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
return renderOutcome.result;
|
|
1078
1188
|
}
|
|
1079
1189
|
}
|