@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- 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 +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +170 -18
- package/src/build/runtime-discovery.ts +13 -1
- 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 +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/index.ts +0 -3
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- package/src/router/segment-wrappers.ts +3 -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 +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- 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/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
package/src/rsc/handler.ts
CHANGED
|
@@ -8,33 +8,44 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { createElement } from "react";
|
|
11
|
-
import { RouteNotFoundError
|
|
11
|
+
import { RouteNotFoundError } from "../errors.js";
|
|
12
12
|
import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
|
|
13
13
|
import {
|
|
14
14
|
runWithRequestContext,
|
|
15
15
|
setRequestContextParams,
|
|
16
16
|
requireRequestContext,
|
|
17
17
|
createRequestContext,
|
|
18
|
-
getLocationState,
|
|
19
18
|
} from "../server/request-context.js";
|
|
20
|
-
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
21
19
|
import * as rscDeps from "@vitejs/plugin-rsc/rsc";
|
|
22
20
|
|
|
23
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
RscPayload,
|
|
23
|
+
CreateRSCHandlerOptions,
|
|
24
|
+
LoadSSRModule,
|
|
25
|
+
SSRModule,
|
|
26
|
+
} from "./types.js";
|
|
24
27
|
import {
|
|
25
28
|
createResponseWithMergedHeaders,
|
|
26
|
-
|
|
29
|
+
finalizeResponse,
|
|
30
|
+
interceptRedirectForPartial,
|
|
31
|
+
buildRouteMiddlewareEntries,
|
|
27
32
|
} from "./helpers.js";
|
|
33
|
+
import {
|
|
34
|
+
handleResponseRoute,
|
|
35
|
+
type ResponseRouteMatch,
|
|
36
|
+
} from "./response-route-handler.js";
|
|
28
37
|
import { generateNonce, nonce as nonceToken } from "./nonce.js";
|
|
29
38
|
import { VERSION } from "@rangojs/router:version";
|
|
30
39
|
import type { ErrorPhase } from "../types.js";
|
|
31
40
|
import type { RouterRequestInput } from "../router/router-interfaces.js";
|
|
32
41
|
import { invokeOnError } from "../router/error-handling.js";
|
|
33
|
-
import {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
createReverseFunction,
|
|
44
|
+
stripInternalParams,
|
|
45
|
+
} from "../router/handler-context.js";
|
|
46
|
+
import { getRouterContext } from "../router/router-context.js";
|
|
47
|
+
import { resolveSink, safeEmit } from "../router/telemetry.js";
|
|
48
|
+
import { contextSet } from "../context-var.js";
|
|
38
49
|
import {
|
|
39
50
|
hasCachedManifest,
|
|
40
51
|
getRouteTrie,
|
|
@@ -44,12 +55,33 @@ import {
|
|
|
44
55
|
getRouterTrie,
|
|
45
56
|
} from "../route-map-builder.js";
|
|
46
57
|
import type { HandlerContext } from "./handler-context.js";
|
|
47
|
-
import { createResponseErrorPayload } from "./response-error.js";
|
|
48
58
|
import { buildRouterTrieFromUrlpatterns } from "./manifest-init.js";
|
|
49
59
|
import { handleProgressiveEnhancement } from "./progressive-enhancement.js";
|
|
50
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
executeServerAction,
|
|
62
|
+
revalidateAfterAction,
|
|
63
|
+
type ActionContinuation,
|
|
64
|
+
} from "./server-action.js";
|
|
51
65
|
import { handleLoaderFetch } from "./loader-fetch.js";
|
|
66
|
+
import { checkRequestOrigin, type OriginCheckPhase } from "./origin-guard.js";
|
|
52
67
|
import { handleRscRendering } from "./rsc-rendering.js";
|
|
68
|
+
import {
|
|
69
|
+
withTimeout,
|
|
70
|
+
RouterTimeoutError,
|
|
71
|
+
createDefaultTimeoutResponse,
|
|
72
|
+
type TimeoutPhase,
|
|
73
|
+
} from "../router/timeout.js";
|
|
74
|
+
import {
|
|
75
|
+
createMetricsStore,
|
|
76
|
+
appendMetric,
|
|
77
|
+
buildMetricsTiming,
|
|
78
|
+
} from "../router/metrics.js";
|
|
79
|
+
import {
|
|
80
|
+
startSSRSetup,
|
|
81
|
+
getSSRSetup,
|
|
82
|
+
mayNeedSSR,
|
|
83
|
+
SSR_SETUP_VAR,
|
|
84
|
+
} from "./ssr-setup.js";
|
|
53
85
|
|
|
54
86
|
/**
|
|
55
87
|
* Create an RSC request handler.
|
|
@@ -84,13 +116,6 @@ import { handleRscRendering } from "./rsc-rendering.js";
|
|
|
84
116
|
* });
|
|
85
117
|
* ```
|
|
86
118
|
*/
|
|
87
|
-
// Only cache successful responses. Non-200 statuses (errors, redirects) are
|
|
88
|
-
// not cached — notFound() produces 500 in response routes, and explicit
|
|
89
|
-
// non-200 Responses are rare enough that caching them would be surprising.
|
|
90
|
-
function isCacheableStatus(status: number): boolean {
|
|
91
|
-
return status === 200;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
119
|
export function createRSCHandler<
|
|
95
120
|
TEnv = unknown,
|
|
96
121
|
TRoutes extends Record<string, string> = Record<string, string>,
|
|
@@ -108,18 +133,28 @@ export function createRSCHandler<
|
|
|
108
133
|
decodeFormState,
|
|
109
134
|
} = deps;
|
|
110
135
|
|
|
111
|
-
// Use provided loadSSRModule or default to vite RSC module loader
|
|
112
|
-
|
|
136
|
+
// Use provided loadSSRModule or default to vite RSC module loader.
|
|
137
|
+
// In production the SSR module is stable across requests, so memoize
|
|
138
|
+
// the dynamic import to avoid repeated module resolution overhead.
|
|
139
|
+
// In dev mode Vite may hot-reload the module, so skip memoization.
|
|
140
|
+
const rawLoadSSRModule: LoadSSRModule =
|
|
113
141
|
options.loadSSRModule ??
|
|
114
142
|
(() => import.meta.viteRsc.loadModule("ssr", "index"));
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
143
|
+
let _ssrModulePromise: Promise<SSRModule> | undefined;
|
|
144
|
+
const loadSSRModule: LoadSSRModule =
|
|
145
|
+
process.env.NODE_ENV === "production"
|
|
146
|
+
? () =>
|
|
147
|
+
(_ssrModulePromise ??= rawLoadSSRModule().catch((err) => {
|
|
148
|
+
_ssrModulePromise = undefined;
|
|
149
|
+
throw err;
|
|
150
|
+
}))
|
|
151
|
+
: rawLoadSSRModule;
|
|
119
152
|
|
|
120
153
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
154
|
+
* Per-request error reporter that deduplicates via the ALS request context.
|
|
155
|
+
*
|
|
156
|
+
* Uses the same _reportedErrors WeakSet as the router layer so errors
|
|
157
|
+
* that propagate across layers are only reported once per request.
|
|
123
158
|
*/
|
|
124
159
|
function callOnError(
|
|
125
160
|
error: unknown,
|
|
@@ -127,6 +162,7 @@ export function createRSCHandler<
|
|
|
127
162
|
context: Parameters<typeof invokeOnError<TEnv>>[3],
|
|
128
163
|
): void {
|
|
129
164
|
if (error != null && typeof error === "object") {
|
|
165
|
+
const reportedErrors = requireRequestContext()._reportedErrors;
|
|
130
166
|
if (reportedErrors.has(error)) return;
|
|
131
167
|
reportedErrors.add(error);
|
|
132
168
|
}
|
|
@@ -143,6 +179,72 @@ export function createRSCHandler<
|
|
|
143
179
|
return routeMap;
|
|
144
180
|
}
|
|
145
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Handle a timeout by reporting the error, emitting telemetry,
|
|
184
|
+
* and returning either the custom onTimeout response or a default 504.
|
|
185
|
+
*/
|
|
186
|
+
async function handleTimeoutResponse(
|
|
187
|
+
request: Request,
|
|
188
|
+
env: TEnv,
|
|
189
|
+
url: URL,
|
|
190
|
+
phase: TimeoutPhase,
|
|
191
|
+
durationMs: number,
|
|
192
|
+
routeKey?: string,
|
|
193
|
+
actionId?: string,
|
|
194
|
+
): Promise<Response> {
|
|
195
|
+
const timeoutError = new RouterTimeoutError(phase, durationMs);
|
|
196
|
+
|
|
197
|
+
callOnError(timeoutError, phase === "action" ? "action" : "handler", {
|
|
198
|
+
request,
|
|
199
|
+
url,
|
|
200
|
+
env,
|
|
201
|
+
routeKey,
|
|
202
|
+
actionId,
|
|
203
|
+
handledByBoundary: false,
|
|
204
|
+
metadata: { timeout: true, phase, durationMs },
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const routerCtx = getRouterContext();
|
|
209
|
+
if (routerCtx?.telemetry) {
|
|
210
|
+
safeEmit(resolveSink(routerCtx.telemetry), {
|
|
211
|
+
type: "request.timeout" as const,
|
|
212
|
+
timestamp: performance.now(),
|
|
213
|
+
requestId: routerCtx.requestId,
|
|
214
|
+
phase,
|
|
215
|
+
pathname: url.pathname,
|
|
216
|
+
routeKey,
|
|
217
|
+
actionId,
|
|
218
|
+
durationMs,
|
|
219
|
+
customHandler: !!router.onTimeout,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// Router context may not be available
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (router.onTimeout) {
|
|
227
|
+
try {
|
|
228
|
+
return await router.onTimeout({
|
|
229
|
+
phase,
|
|
230
|
+
request,
|
|
231
|
+
url,
|
|
232
|
+
env,
|
|
233
|
+
routeKey,
|
|
234
|
+
actionId,
|
|
235
|
+
durationMs,
|
|
236
|
+
});
|
|
237
|
+
} catch (e) {
|
|
238
|
+
if (process.env.NODE_ENV !== "production") {
|
|
239
|
+
console.error("[RSC] onTimeout callback error:", e);
|
|
240
|
+
}
|
|
241
|
+
return createDefaultTimeoutResponse(phase);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return createDefaultTimeoutResponse(phase);
|
|
246
|
+
}
|
|
247
|
+
|
|
146
248
|
/**
|
|
147
249
|
* Build a 200 Flight response that carries a redirect URL and optional state.
|
|
148
250
|
* Used when a partial/action request results in a redirect -- fetch
|
|
@@ -167,7 +269,8 @@ export function createRSCHandler<
|
|
|
167
269
|
});
|
|
168
270
|
}
|
|
169
271
|
|
|
170
|
-
// Bundle shared dependencies for extracted handler functions
|
|
272
|
+
// Bundle shared dependencies for extracted handler functions.
|
|
273
|
+
// callOnError reads from ALS so it's inherently per-request scoped.
|
|
171
274
|
const handlerCtx: HandlerContext<TEnv> = {
|
|
172
275
|
router,
|
|
173
276
|
version,
|
|
@@ -181,6 +284,11 @@ export function createRSCHandler<
|
|
|
181
284
|
callOnError,
|
|
182
285
|
getRequiredRouteMap,
|
|
183
286
|
createRedirectFlightResponse,
|
|
287
|
+
resolveStreamMode: async (request, env, url) => {
|
|
288
|
+
const resolver = router.ssr?.resolveStreaming;
|
|
289
|
+
if (!resolver) return "stream";
|
|
290
|
+
return resolver({ request, env, url });
|
|
291
|
+
},
|
|
184
292
|
};
|
|
185
293
|
|
|
186
294
|
return async function handler(
|
|
@@ -188,6 +296,11 @@ export function createRSCHandler<
|
|
|
188
296
|
input: RouterRequestInput<TEnv> = {},
|
|
189
297
|
): Promise<Response> {
|
|
190
298
|
const handlerStart = performance.now();
|
|
299
|
+
// Create the metrics store at handler start so handler:total has startTime=0
|
|
300
|
+
// and all metrics are relative to the request entry point.
|
|
301
|
+
const earlyMetricsStore = router.debugPerformance
|
|
302
|
+
? createMetricsStore(true, handlerStart)
|
|
303
|
+
: undefined;
|
|
191
304
|
|
|
192
305
|
const { env = {} as TEnv, vars: initialVars, ctx: executionCtx } = input;
|
|
193
306
|
|
|
@@ -287,9 +400,6 @@ export function createRSCHandler<
|
|
|
287
400
|
}
|
|
288
401
|
const manifestCacheDur = performance.now() - manifestCacheStart;
|
|
289
402
|
|
|
290
|
-
// Note: Route map for useHref() is loaded lazily via getGlobalRouteMap()
|
|
291
|
-
// This allows it to include all routes from lazy includes after manifest loading
|
|
292
|
-
|
|
293
403
|
// Create unified request context with all methods
|
|
294
404
|
// Includes: stub response, handle store, loader memoization, use(), cookies, headers, cache store
|
|
295
405
|
// params starts empty, populated after route matching via setRequestContextParams
|
|
@@ -300,9 +410,27 @@ export function createRSCHandler<
|
|
|
300
410
|
url,
|
|
301
411
|
variables,
|
|
302
412
|
cacheStore,
|
|
413
|
+
cacheProfiles: router.cacheProfiles,
|
|
303
414
|
executionContext: executionCtx,
|
|
304
415
|
themeConfig: router.themeConfig,
|
|
305
416
|
});
|
|
417
|
+
if (earlyMetricsStore) {
|
|
418
|
+
requestContext._debugPerformance = true;
|
|
419
|
+
requestContext._metricsStore = earlyMetricsStore;
|
|
420
|
+
}
|
|
421
|
+
// Wire background error reporting so "use cache" and other subsystems
|
|
422
|
+
// can surface non-fatal errors through the router's onError callback.
|
|
423
|
+
requestContext._reportBackgroundError = (
|
|
424
|
+
error: unknown,
|
|
425
|
+
category: string,
|
|
426
|
+
) => {
|
|
427
|
+
callOnError(error, "cache", {
|
|
428
|
+
request,
|
|
429
|
+
url,
|
|
430
|
+
metadata: { category },
|
|
431
|
+
});
|
|
432
|
+
};
|
|
433
|
+
|
|
306
434
|
const ctxCreateDur = performance.now() - ctxCreateStart;
|
|
307
435
|
|
|
308
436
|
// Accumulate handler-level timing for Server-Timing header
|
|
@@ -331,6 +459,7 @@ export function createRSCHandler<
|
|
|
331
459
|
};
|
|
332
460
|
|
|
333
461
|
// Execute middleware chain if any, otherwise call core handler directly
|
|
462
|
+
let response: Response;
|
|
334
463
|
if (matchedMiddleware.length > 0) {
|
|
335
464
|
const mwResponse = await executeMiddleware(
|
|
336
465
|
matchedMiddleware,
|
|
@@ -341,30 +470,60 @@ export function createRSCHandler<
|
|
|
341
470
|
createReverseFunction(getRequiredRouteMap()),
|
|
342
471
|
);
|
|
343
472
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (locationState) {
|
|
356
|
-
return createRedirectFlightResponse(
|
|
357
|
-
redirectUrl,
|
|
358
|
-
resolveLocationStateEntries(locationState),
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
return createSimpleRedirectResponse(redirectUrl);
|
|
473
|
+
if (
|
|
474
|
+
url.searchParams.has("_rsc_partial") ||
|
|
475
|
+
url.searchParams.has("_rsc_action")
|
|
476
|
+
) {
|
|
477
|
+
const intercepted = interceptRedirectForPartial(
|
|
478
|
+
mwResponse,
|
|
479
|
+
createRedirectFlightResponse,
|
|
480
|
+
);
|
|
481
|
+
response = intercepted ?? finalizeResponse(mwResponse);
|
|
482
|
+
} else {
|
|
483
|
+
response = finalizeResponse(mwResponse);
|
|
362
484
|
}
|
|
485
|
+
} else {
|
|
486
|
+
response = await coreHandler();
|
|
487
|
+
}
|
|
363
488
|
|
|
364
|
-
|
|
489
|
+
// Finalize metrics after all middleware (including post-next work)
|
|
490
|
+
// has completed so :post spans are captured in the timeline.
|
|
491
|
+
// Handler timing parts are always emitted (even without debug metrics)
|
|
492
|
+
// so non-debug requests still get bootstrap Server-Timing entries.
|
|
493
|
+
const handlerTimingArr: string[] = variables.__handlerTiming || [];
|
|
494
|
+
// Preserve any existing Server-Timing set by response routes or middleware
|
|
495
|
+
const existingTiming = response.headers.get("Server-Timing");
|
|
496
|
+
const timingParts = existingTiming
|
|
497
|
+
? [existingTiming, ...handlerTimingArr]
|
|
498
|
+
: [...handlerTimingArr];
|
|
499
|
+
|
|
500
|
+
const metricsStore = requestContext._metricsStore;
|
|
501
|
+
if (metricsStore) {
|
|
502
|
+
// When the store was created at handler start (earlyMetricsStore),
|
|
503
|
+
// handler:total covers the full request. When ctx.debugPerformance()
|
|
504
|
+
// created the store mid-request, use its requestStart to avoid a
|
|
505
|
+
// negative startTime offset.
|
|
506
|
+
const totalStart = earlyMetricsStore
|
|
507
|
+
? handlerStart
|
|
508
|
+
: metricsStore.requestStart;
|
|
509
|
+
appendMetric(
|
|
510
|
+
metricsStore,
|
|
511
|
+
"handler:total",
|
|
512
|
+
totalStart,
|
|
513
|
+
performance.now() - totalStart,
|
|
514
|
+
);
|
|
515
|
+
const metricsTiming = buildMetricsTiming(
|
|
516
|
+
request.method,
|
|
517
|
+
url.pathname,
|
|
518
|
+
metricsStore,
|
|
519
|
+
);
|
|
520
|
+
if (metricsTiming) timingParts.push(metricsTiming);
|
|
365
521
|
}
|
|
366
522
|
|
|
367
|
-
|
|
523
|
+
const fullTiming = timingParts.join(", ");
|
|
524
|
+
if (fullTiming) response.headers.set("Server-Timing", fullTiming);
|
|
525
|
+
|
|
526
|
+
return response;
|
|
368
527
|
});
|
|
369
528
|
};
|
|
370
529
|
|
|
@@ -376,7 +535,6 @@ export function createRSCHandler<
|
|
|
376
535
|
variables: Record<string, any>,
|
|
377
536
|
nonce: string | undefined,
|
|
378
537
|
): Promise<Response> {
|
|
379
|
-
// First, check for route-level middleware
|
|
380
538
|
const previewStart = performance.now();
|
|
381
539
|
const preview = await router.previewMatch(request, { env });
|
|
382
540
|
const previewDur = performance.now() - previewStart;
|
|
@@ -384,286 +542,224 @@ export function createRSCHandler<
|
|
|
384
542
|
handlerTiming.push(`handler-preview-match;dur=${previewDur.toFixed(2)}`);
|
|
385
543
|
// Response route short-circuit: skip entire RSC pipeline
|
|
386
544
|
if (preview?.responseType && preview.handler) {
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
return
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
545
|
+
const responseOutcome = await withTimeout(
|
|
546
|
+
handleResponseRoute(
|
|
547
|
+
handlerCtx,
|
|
548
|
+
preview as ResponseRouteMatch,
|
|
549
|
+
request,
|
|
550
|
+
env,
|
|
551
|
+
url,
|
|
552
|
+
variables,
|
|
553
|
+
),
|
|
554
|
+
router.timeouts.renderStartMs,
|
|
555
|
+
"render-start",
|
|
556
|
+
);
|
|
557
|
+
if (responseOutcome.timedOut) {
|
|
558
|
+
return handleTimeoutResponse(
|
|
559
|
+
request,
|
|
560
|
+
env,
|
|
561
|
+
url,
|
|
562
|
+
"render-start",
|
|
563
|
+
responseOutcome.durationMs,
|
|
564
|
+
preview?.routeKey,
|
|
565
|
+
);
|
|
407
566
|
}
|
|
567
|
+
return responseOutcome.result;
|
|
568
|
+
}
|
|
408
569
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
570
|
+
// Kick off SSR module loading + stream mode resolution in parallel with
|
|
571
|
+
// segment resolution. Placed after the response-route short-circuit so
|
|
572
|
+
// response/mime routes never pay for SSR work.
|
|
573
|
+
if (mayNeedSSR(request, url)) {
|
|
574
|
+
variables[SSR_SETUP_VAR] = startSSRSetup(
|
|
575
|
+
handlerCtx,
|
|
412
576
|
request,
|
|
413
|
-
params: preview.params || {},
|
|
414
577
|
env,
|
|
415
|
-
searchParams: url.searchParams,
|
|
416
578
|
url,
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
579
|
+
router.debugPerformance
|
|
580
|
+
? () => requireRequestContext()._metricsStore
|
|
581
|
+
: undefined,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const routeReverse = createReverseFunction(getRequiredRouteMap());
|
|
586
|
+
|
|
587
|
+
const isAction =
|
|
588
|
+
request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
|
|
589
|
+
const isLoaderFetch = url.searchParams.has("_rsc_loader");
|
|
590
|
+
const actionId =
|
|
591
|
+
request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
|
|
592
|
+
|
|
593
|
+
// Origin guard: reject cross-origin actions, loader fetches, and
|
|
594
|
+
// PE form submissions before any execution. Regular page navigations
|
|
595
|
+
// (GET without _rsc_loader/_rsc_action) are not affected.
|
|
596
|
+
const originPhase: OriginCheckPhase | null = isAction
|
|
597
|
+
? "action"
|
|
598
|
+
: isLoaderFetch
|
|
599
|
+
? "loader"
|
|
600
|
+
: request.method === "POST"
|
|
601
|
+
? "pe-form"
|
|
602
|
+
: null;
|
|
603
|
+
if (originPhase) {
|
|
604
|
+
const originResult = await checkRequestOrigin(
|
|
605
|
+
request,
|
|
606
|
+
url,
|
|
607
|
+
router.originCheck,
|
|
608
|
+
env,
|
|
609
|
+
router.id,
|
|
610
|
+
originPhase,
|
|
611
|
+
);
|
|
612
|
+
if (originResult) {
|
|
613
|
+
const originError = new Error(
|
|
614
|
+
`Origin check rejected: ${request.headers.get("origin") ?? "none"} vs ${request.headers.get("host") ?? "none"}`,
|
|
615
|
+
);
|
|
616
|
+
originError.name = "OriginCheckError";
|
|
617
|
+
|
|
618
|
+
callOnError(originError, "origin", {
|
|
619
|
+
request,
|
|
620
|
+
url,
|
|
621
|
+
env,
|
|
622
|
+
handledByBoundary: false,
|
|
623
|
+
metadata: {
|
|
624
|
+
phase: originPhase,
|
|
625
|
+
origin: request.headers.get("origin"),
|
|
626
|
+
host: request.headers.get("host"),
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
const routerCtx = getRouterContext();
|
|
632
|
+
if (routerCtx?.telemetry) {
|
|
633
|
+
safeEmit(resolveSink(routerCtx.telemetry), {
|
|
634
|
+
type: "request.origin-rejected" as const,
|
|
635
|
+
timestamp: performance.now(),
|
|
636
|
+
requestId: routerCtx.requestId,
|
|
637
|
+
method: request.method,
|
|
638
|
+
pathname: url.pathname,
|
|
639
|
+
phase: originPhase,
|
|
640
|
+
origin: request.headers.get("origin"),
|
|
641
|
+
host: request.headers.get("host"),
|
|
426
642
|
});
|
|
427
643
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
431
|
-
header: (name: string, value: string) => reqCtx.header(name, value),
|
|
432
|
-
setCookie: (name: string, value: string, options?: any) =>
|
|
433
|
-
reqCtx.setCookie(name, value, options),
|
|
434
|
-
_responseType: preview.responseType,
|
|
435
|
-
};
|
|
436
|
-
// Brand with taint symbol so "use cache" detects it as request-scoped
|
|
437
|
-
// and extracts route-identifying properties (params, pathname, _responseType)
|
|
438
|
-
(responseHandlerCtx as any)[NOCACHE_SYMBOL] = true;
|
|
439
|
-
|
|
440
|
-
// Call handler directly, wrapped by route middleware if present
|
|
441
|
-
const callHandler = async () => {
|
|
442
|
-
// JSON response routes: wrap in { data } / { error } envelope
|
|
443
|
-
if (preview.responseType === "json") {
|
|
444
|
-
const errorCtx = { request, url, env };
|
|
445
|
-
try {
|
|
446
|
-
const result = await (preview.handler as Function)(
|
|
447
|
-
responseHandlerCtx,
|
|
448
|
-
);
|
|
449
|
-
if (result instanceof Response) {
|
|
450
|
-
const mergedHeaders: Record<string, string> = {};
|
|
451
|
-
result.headers.forEach((value, key) => {
|
|
452
|
-
mergedHeaders[key] = value;
|
|
453
|
-
});
|
|
454
|
-
return createResponseWithMergedHeaders(result.body, {
|
|
455
|
-
status: result.status,
|
|
456
|
-
headers: mergedHeaders,
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
return createResponseWithMergedHeaders(
|
|
460
|
-
JSON.stringify({ data: result }),
|
|
461
|
-
{
|
|
462
|
-
status: 200,
|
|
463
|
-
headers: { "content-type": "application/json;charset=utf-8" },
|
|
464
|
-
},
|
|
465
|
-
);
|
|
466
|
-
} catch (error) {
|
|
467
|
-
callOnError(error, "handler", errorCtx);
|
|
468
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
469
|
-
const status = error instanceof RouterError ? error.status : 500;
|
|
470
|
-
return createResponseWithMergedHeaders(
|
|
471
|
-
JSON.stringify({
|
|
472
|
-
error: createResponseErrorPayload(error, isDev),
|
|
473
|
-
}),
|
|
474
|
-
{
|
|
475
|
-
status,
|
|
476
|
-
headers: { "content-type": "application/json;charset=utf-8" },
|
|
477
|
-
},
|
|
478
|
-
);
|
|
479
|
-
}
|
|
644
|
+
} catch {
|
|
645
|
+
// Router context may not be available
|
|
480
646
|
}
|
|
481
647
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const result = await (preview.handler as Function)(
|
|
486
|
-
responseHandlerCtx,
|
|
487
|
-
);
|
|
648
|
+
return originResult;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
488
651
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const mergedHeaders: Record<string, string> = {};
|
|
492
|
-
result.headers.forEach((value, key) => {
|
|
493
|
-
mergedHeaders[key] = value;
|
|
494
|
-
});
|
|
495
|
-
return createResponseWithMergedHeaders(result.body, {
|
|
496
|
-
status: result.status,
|
|
497
|
-
headers: mergedHeaders,
|
|
498
|
-
});
|
|
499
|
-
}
|
|
652
|
+
// Get handle store from request context
|
|
653
|
+
const handleStore = requireRequestContext()._handleStore;
|
|
500
654
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
`Response route handler for "${preview.responseType}" must return a Response object, got ${typeof result}`,
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
} catch (error) {
|
|
530
|
-
callOnError(error, "handler", errorCtx);
|
|
531
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
532
|
-
const status = error instanceof RouterError ? error.status : 500;
|
|
533
|
-
const message =
|
|
534
|
-
error instanceof RouterError
|
|
535
|
-
? error.message
|
|
536
|
-
: isDev && error instanceof Error
|
|
537
|
-
? error.message
|
|
538
|
-
: "Internal Server Error";
|
|
539
|
-
return createResponseWithMergedHeaders(message, {
|
|
540
|
-
status,
|
|
541
|
-
headers: { "content-type": "text/plain;charset=utf-8" },
|
|
655
|
+
// Wire up error reporting for late streaming-handle failures
|
|
656
|
+
// (LateHandlePushError: handle pushed after stream completion).
|
|
657
|
+
// Without this, these errors are only caught by React's error boundary
|
|
658
|
+
// and never reach the router's onError callback or telemetry.
|
|
659
|
+
handleStore.onError = (error: Error) => {
|
|
660
|
+
const reqCtx = requireRequestContext();
|
|
661
|
+
callOnError(error, "handler", {
|
|
662
|
+
request,
|
|
663
|
+
url,
|
|
664
|
+
routeKey: reqCtx._routeName,
|
|
665
|
+
params: reqCtx.params as Record<string, string>,
|
|
666
|
+
handledByBoundary: true,
|
|
667
|
+
});
|
|
668
|
+
try {
|
|
669
|
+
const routerCtx = getRouterContext();
|
|
670
|
+
if (routerCtx?.telemetry) {
|
|
671
|
+
safeEmit(resolveSink(routerCtx.telemetry), {
|
|
672
|
+
type: "handler.error" as const,
|
|
673
|
+
timestamp: performance.now(),
|
|
674
|
+
requestId: routerCtx.requestId,
|
|
675
|
+
error,
|
|
676
|
+
handledByBoundary: true,
|
|
677
|
+
pathname: url.pathname,
|
|
678
|
+
routeKey: reqCtx._routeName,
|
|
679
|
+
params: reqCtx.params as Record<string, string>,
|
|
542
680
|
});
|
|
543
681
|
}
|
|
544
|
-
}
|
|
682
|
+
} catch {
|
|
683
|
+
// Router context may not be available (e.g. prerender path)
|
|
684
|
+
}
|
|
685
|
+
};
|
|
545
686
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
response.headers.append("Vary", "Accept");
|
|
551
|
-
}
|
|
552
|
-
return response;
|
|
553
|
-
};
|
|
687
|
+
// Set route params early so all execution paths can access ctx.params.
|
|
688
|
+
if (preview?.params) {
|
|
689
|
+
setRequestContextParams(preview.params, preview.routeKey);
|
|
690
|
+
}
|
|
554
691
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
692
|
+
// Progressive enhancement runs before the normal action/render paths.
|
|
693
|
+
// Route middleware wraps the PE re-render so handlers see the same
|
|
694
|
+
// context variables regardless of JS/no-JS transport.
|
|
695
|
+
const progressiveResult = await handleProgressiveEnhancement(
|
|
696
|
+
handlerCtx,
|
|
697
|
+
request,
|
|
698
|
+
env,
|
|
699
|
+
url,
|
|
700
|
+
isAction,
|
|
701
|
+
handleStore,
|
|
702
|
+
nonce,
|
|
703
|
+
{
|
|
704
|
+
routeMiddleware: preview?.routeMiddleware,
|
|
705
|
+
variables,
|
|
706
|
+
routeReverse,
|
|
707
|
+
},
|
|
708
|
+
);
|
|
709
|
+
if (progressiveResult) {
|
|
710
|
+
return progressiveResult;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// --- Action execution: runs BEFORE route middleware ---
|
|
714
|
+
// Route middleware wraps rendering only. For actions, the action runs
|
|
715
|
+
// first in the global middleware context, then route middleware wraps
|
|
716
|
+
// the revalidation pass (identical to a normal render).
|
|
717
|
+
let actionContinuation: ActionContinuation | undefined;
|
|
718
|
+
if (isAction && actionId) {
|
|
719
|
+
try {
|
|
720
|
+
const actionOutcome = await withTimeout(
|
|
721
|
+
executeServerAction(
|
|
722
|
+
handlerCtx,
|
|
570
723
|
request,
|
|
571
724
|
env,
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
725
|
+
url,
|
|
726
|
+
actionId,
|
|
727
|
+
handleStore,
|
|
728
|
+
),
|
|
729
|
+
router.timeouts.actionMs,
|
|
730
|
+
"action",
|
|
731
|
+
);
|
|
732
|
+
if (actionOutcome.timedOut) {
|
|
733
|
+
return handleTimeoutResponse(
|
|
734
|
+
request,
|
|
735
|
+
env,
|
|
736
|
+
url,
|
|
737
|
+
"action",
|
|
738
|
+
actionOutcome.durationMs,
|
|
739
|
+
preview?.routeKey,
|
|
740
|
+
actionId,
|
|
575
741
|
);
|
|
576
742
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const store = cacheScope.getStore() ?? reqCtx._cacheStore;
|
|
592
|
-
if (store?.getResponse && store?.putResponse) {
|
|
593
|
-
// Build cache key with response:{type}: prefix to avoid collision
|
|
594
|
-
// with segment keys and differentiate between response types
|
|
595
|
-
let cacheKey = `response:${preview.responseType}:${url.pathname}`;
|
|
596
|
-
if (store.keyGenerator) {
|
|
597
|
-
try {
|
|
598
|
-
cacheKey = await store.keyGenerator(reqCtx, cacheKey);
|
|
599
|
-
} catch {
|
|
600
|
-
// Fall back to default key on keyGenerator failure
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
try {
|
|
605
|
-
const cached = await store.getResponse(cacheKey);
|
|
606
|
-
|
|
607
|
-
if (cached && isCacheableStatus(cached.response.status)) {
|
|
608
|
-
if (!cached.shouldRevalidate) {
|
|
609
|
-
// Fresh hit
|
|
610
|
-
return cached.response;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Stale hit (SWR) - return cached, revalidate in background
|
|
614
|
-
reqCtx.waitUntil(async () => {
|
|
615
|
-
try {
|
|
616
|
-
const fresh = await executeHandler();
|
|
617
|
-
if (isCacheableStatus(fresh.status)) {
|
|
618
|
-
await store.putResponse!(
|
|
619
|
-
cacheKey,
|
|
620
|
-
fresh,
|
|
621
|
-
cacheScope!.ttl,
|
|
622
|
-
cacheScope!.swr,
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
} catch (error) {
|
|
626
|
-
console.error(
|
|
627
|
-
`[ResponseCache] Revalidation failed:`,
|
|
628
|
-
error,
|
|
629
|
-
);
|
|
630
|
-
}
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
return cached.response;
|
|
634
|
-
}
|
|
635
|
-
} catch (error) {
|
|
636
|
-
console.error(`[ResponseCache] Cache lookup failed:`, error);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// Cache miss - execute handler and cache the result
|
|
640
|
-
const response = await executeHandler();
|
|
641
|
-
|
|
642
|
-
if (isCacheableStatus(response.status)) {
|
|
643
|
-
reqCtx.waitUntil(async () => {
|
|
644
|
-
try {
|
|
645
|
-
await store.putResponse!(
|
|
646
|
-
cacheKey,
|
|
647
|
-
response.clone(),
|
|
648
|
-
cacheScope!.ttl,
|
|
649
|
-
cacheScope!.swr,
|
|
650
|
-
);
|
|
651
|
-
} catch (error) {
|
|
652
|
-
console.error(`[ResponseCache] Cache write failed:`, error);
|
|
653
|
-
}
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
return response;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
743
|
+
const result = actionOutcome.result;
|
|
744
|
+
// Response means redirect or error boundary — done.
|
|
745
|
+
if (result instanceof Response) return result;
|
|
746
|
+
actionContinuation = result;
|
|
747
|
+
} catch (error) {
|
|
748
|
+
callOnError(error, "action", {
|
|
749
|
+
request,
|
|
750
|
+
url,
|
|
751
|
+
env,
|
|
752
|
+
actionId,
|
|
753
|
+
handledByBoundary: false,
|
|
754
|
+
});
|
|
755
|
+
console.error(`[RSC] Action error:`, error);
|
|
756
|
+
throw error;
|
|
660
757
|
}
|
|
661
|
-
|
|
662
|
-
return executeHandler();
|
|
663
758
|
}
|
|
664
759
|
|
|
665
|
-
//
|
|
666
|
-
|
|
760
|
+
// --- Rendering (action revalidation or navigation) ---
|
|
761
|
+
// Route middleware wraps this — same code path for both cases.
|
|
762
|
+
const renderHandler = async () => {
|
|
667
763
|
const response = await coreRequestHandlerInner(
|
|
668
764
|
request,
|
|
669
765
|
env,
|
|
@@ -672,6 +768,8 @@ export function createRSCHandler<
|
|
|
672
768
|
nonce,
|
|
673
769
|
preview?.params,
|
|
674
770
|
preview?.routeKey,
|
|
771
|
+
handleStore,
|
|
772
|
+
actionContinuation,
|
|
675
773
|
);
|
|
676
774
|
if (preview?.negotiated) {
|
|
677
775
|
response.headers.append("Vary", "Accept");
|
|
@@ -679,57 +777,58 @@ export function createRSCHandler<
|
|
|
679
777
|
return response;
|
|
680
778
|
};
|
|
681
779
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
// Execute route middleware wrapping the actual request handling
|
|
696
|
-
const mwResponse = await executeMiddleware(
|
|
697
|
-
middlewareEntries,
|
|
698
|
-
request,
|
|
699
|
-
env,
|
|
700
|
-
variables,
|
|
701
|
-
rscHandler,
|
|
702
|
-
createReverseFunction(getRequiredRouteMap()),
|
|
703
|
-
);
|
|
780
|
+
// Wrap the render path (with or without route middleware) in a
|
|
781
|
+
// renderStartMs timeout so slow renders are caught before output.
|
|
782
|
+
const executeRender = async (): Promise<Response> => {
|
|
783
|
+
if (preview?.routeMiddleware && preview.routeMiddleware.length > 0) {
|
|
784
|
+
const mwResponse = await executeMiddleware(
|
|
785
|
+
buildRouteMiddlewareEntries<TEnv>(preview.routeMiddleware),
|
|
786
|
+
request,
|
|
787
|
+
env,
|
|
788
|
+
variables,
|
|
789
|
+
renderHandler,
|
|
790
|
+
routeReverse,
|
|
791
|
+
);
|
|
704
792
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const isMwRedirect =
|
|
713
|
-
mwResponse.status >= 300 && mwResponse.status < 400 && mwRedirectUrl;
|
|
714
|
-
if (isPartial && isMwRedirect) {
|
|
715
|
-
const locationState = getLocationState();
|
|
716
|
-
if (locationState) {
|
|
717
|
-
return createRedirectFlightResponse(
|
|
718
|
-
mwRedirectUrl,
|
|
719
|
-
resolveLocationStateEntries(locationState),
|
|
793
|
+
if (
|
|
794
|
+
url.searchParams.has("_rsc_partial") ||
|
|
795
|
+
url.searchParams.has("_rsc_action")
|
|
796
|
+
) {
|
|
797
|
+
const intercepted = interceptRedirectForPartial(
|
|
798
|
+
mwResponse,
|
|
799
|
+
createRedirectFlightResponse,
|
|
720
800
|
);
|
|
801
|
+
if (intercepted) return intercepted;
|
|
721
802
|
}
|
|
722
|
-
|
|
803
|
+
|
|
804
|
+
return finalizeResponse(mwResponse);
|
|
723
805
|
}
|
|
724
806
|
|
|
725
|
-
|
|
726
|
-
|
|
807
|
+
// No route middleware, proceed directly
|
|
808
|
+
return renderHandler();
|
|
809
|
+
};
|
|
727
810
|
|
|
728
|
-
|
|
729
|
-
|
|
811
|
+
const renderOutcome = await withTimeout(
|
|
812
|
+
executeRender(),
|
|
813
|
+
router.timeouts.renderStartMs,
|
|
814
|
+
"render-start",
|
|
815
|
+
);
|
|
816
|
+
if (renderOutcome.timedOut) {
|
|
817
|
+
return handleTimeoutResponse(
|
|
818
|
+
request,
|
|
819
|
+
env,
|
|
820
|
+
url,
|
|
821
|
+
"render-start",
|
|
822
|
+
renderOutcome.durationMs,
|
|
823
|
+
preview?.routeKey,
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
return renderOutcome.result;
|
|
730
827
|
}
|
|
731
828
|
|
|
732
|
-
// Inner request handler
|
|
829
|
+
// Inner request handler: rendering logic wrapped by route middleware.
|
|
830
|
+
// Handles action revalidation (when actionContinuation is present),
|
|
831
|
+
// loader fetches, and regular RSC rendering.
|
|
733
832
|
async function coreRequestHandlerInner(
|
|
734
833
|
request: Request,
|
|
735
834
|
env: TEnv,
|
|
@@ -738,12 +837,12 @@ export function createRSCHandler<
|
|
|
738
837
|
nonce: string | undefined,
|
|
739
838
|
routeParams?: Record<string, string>,
|
|
740
839
|
routeKey?: string,
|
|
840
|
+
handleStore?: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
841
|
+
actionContinuation?: ActionContinuation,
|
|
741
842
|
): Promise<Response> {
|
|
742
843
|
const isPartial = url.searchParams.has("_rsc_partial");
|
|
743
844
|
const isAction =
|
|
744
845
|
request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
|
|
745
|
-
const actionId =
|
|
746
|
-
request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
|
|
747
846
|
|
|
748
847
|
// Version mismatch detection - client may have stale code after HMR/deployment
|
|
749
848
|
// If versions don't match, tell the client to reload
|
|
@@ -753,19 +852,10 @@ export function createRSCHandler<
|
|
|
753
852
|
`[RSC] Version mismatch: client=${clientVersion}, server=${version}. Forcing reload.`,
|
|
754
853
|
);
|
|
755
854
|
|
|
756
|
-
// Clean URL by removing RSC params
|
|
757
|
-
const cleanUrl = new URL(url);
|
|
758
|
-
cleanUrl.searchParams.delete("_rsc_partial");
|
|
759
|
-
cleanUrl.searchParams.delete("_rsc_segments");
|
|
760
|
-
cleanUrl.searchParams.delete("_rsc_v");
|
|
761
|
-
cleanUrl.searchParams.delete("_rsc_stale");
|
|
762
|
-
cleanUrl.searchParams.delete("_rsc_action");
|
|
763
|
-
cleanUrl.searchParams.delete("_rsc_prev");
|
|
764
|
-
|
|
765
855
|
// For actions, reload current page (referer) if same origin.
|
|
766
856
|
// For navigation, load the target URL.
|
|
767
857
|
// Validate referer origin to prevent open redirect via crafted header.
|
|
768
|
-
let reloadUrl =
|
|
858
|
+
let reloadUrl = stripInternalParams(url).toString();
|
|
769
859
|
if (isAction) {
|
|
770
860
|
const referer = request.headers.get("referer");
|
|
771
861
|
if (referer) {
|
|
@@ -818,57 +908,27 @@ export function createRSCHandler<
|
|
|
818
908
|
);
|
|
819
909
|
}
|
|
820
910
|
|
|
821
|
-
|
|
822
|
-
const handleStore = requireRequestContext()._handleStore;
|
|
911
|
+
const store = handleStore ?? requireRequestContext()._handleStore;
|
|
823
912
|
|
|
824
913
|
try {
|
|
825
|
-
//
|
|
826
|
-
//
|
|
827
|
-
// Previously this was only done for JS actions, leaving PE actions with empty params.
|
|
914
|
+
// Route params were already set in coreRequestHandler, but set again
|
|
915
|
+
// for callers that enter coreRequestHandlerInner directly.
|
|
828
916
|
if (routeParams) {
|
|
829
917
|
setRequestContextParams(routeParams, routeKey);
|
|
830
918
|
}
|
|
831
919
|
|
|
832
920
|
// ============================================================================
|
|
833
|
-
//
|
|
921
|
+
// ACTION REVALIDATION (action already executed, revalidate segments)
|
|
834
922
|
// ============================================================================
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
if (progressiveResult) {
|
|
845
|
-
return progressiveResult;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// ============================================================================
|
|
849
|
-
// SERVER ACTION EXECUTION (JavaScript-enabled client)
|
|
850
|
-
// ============================================================================
|
|
851
|
-
if (isAction && actionId) {
|
|
852
|
-
try {
|
|
853
|
-
return await handleServerAction(
|
|
854
|
-
handlerCtx,
|
|
855
|
-
request,
|
|
856
|
-
env,
|
|
857
|
-
url,
|
|
858
|
-
actionId,
|
|
859
|
-
handleStore,
|
|
860
|
-
);
|
|
861
|
-
} catch (error) {
|
|
862
|
-
callOnError(error, "action", {
|
|
863
|
-
request,
|
|
864
|
-
url,
|
|
865
|
-
env,
|
|
866
|
-
actionId,
|
|
867
|
-
handledByBoundary: false,
|
|
868
|
-
});
|
|
869
|
-
console.error(`[RSC] Action error:`, error);
|
|
870
|
-
throw error;
|
|
871
|
-
}
|
|
923
|
+
if (actionContinuation) {
|
|
924
|
+
return await revalidateAfterAction(
|
|
925
|
+
handlerCtx,
|
|
926
|
+
request,
|
|
927
|
+
env,
|
|
928
|
+
url,
|
|
929
|
+
store,
|
|
930
|
+
actionContinuation,
|
|
931
|
+
);
|
|
872
932
|
}
|
|
873
933
|
|
|
874
934
|
// ============================================================================
|
|
@@ -896,7 +956,7 @@ export function createRSCHandler<
|
|
|
896
956
|
env,
|
|
897
957
|
url,
|
|
898
958
|
isPartial,
|
|
899
|
-
|
|
959
|
+
store,
|
|
900
960
|
nonce,
|
|
901
961
|
);
|
|
902
962
|
} catch (error) {
|
|
@@ -912,39 +972,21 @@ export function createRSCHandler<
|
|
|
912
972
|
`[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
|
|
913
973
|
`Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
|
|
914
974
|
);
|
|
915
|
-
const cleanUrl = new URL(url);
|
|
916
|
-
cleanUrl.searchParams.delete("_rsc_partial");
|
|
917
|
-
cleanUrl.searchParams.delete("_rsc_segments");
|
|
918
|
-
cleanUrl.searchParams.delete("_rsc_v");
|
|
919
|
-
cleanUrl.searchParams.delete("_rsc_stale");
|
|
920
|
-
cleanUrl.searchParams.delete("_rsc_action");
|
|
921
|
-
cleanUrl.searchParams.delete("_rsc_prev");
|
|
922
975
|
return createResponseWithMergedHeaders(null, {
|
|
923
976
|
status: 200,
|
|
924
977
|
headers: {
|
|
925
|
-
"X-RSC-Reload":
|
|
978
|
+
"X-RSC-Reload": stripInternalParams(url).toString(),
|
|
926
979
|
"content-type": "text/x-component;charset=utf-8",
|
|
927
980
|
},
|
|
928
981
|
});
|
|
929
982
|
}
|
|
930
983
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
const isRedirect =
|
|
938
|
-
error.status >= 300 && error.status < 400 && redirectUrl;
|
|
939
|
-
if (isPartial && isRedirect) {
|
|
940
|
-
const locationState = getLocationState();
|
|
941
|
-
if (locationState) {
|
|
942
|
-
return createRedirectFlightResponse(
|
|
943
|
-
redirectUrl,
|
|
944
|
-
resolveLocationStateEntries(locationState),
|
|
945
|
-
);
|
|
946
|
-
}
|
|
947
|
-
return createSimpleRedirectResponse(redirectUrl);
|
|
984
|
+
if (isPartial) {
|
|
985
|
+
const intercepted = interceptRedirectForPartial(
|
|
986
|
+
error,
|
|
987
|
+
createRedirectFlightResponse,
|
|
988
|
+
);
|
|
989
|
+
if (intercepted) return intercepted;
|
|
948
990
|
}
|
|
949
991
|
|
|
950
992
|
return error;
|
|
@@ -988,7 +1030,7 @@ export function createRSCHandler<
|
|
|
988
1030
|
diff: [],
|
|
989
1031
|
isPartial: false,
|
|
990
1032
|
rootLayout: router.rootLayout,
|
|
991
|
-
handles:
|
|
1033
|
+
handles: store.stream(),
|
|
992
1034
|
version,
|
|
993
1035
|
themeConfig: router.themeConfig,
|
|
994
1036
|
warmupEnabled: router.warmupEnabled,
|
|
@@ -1014,9 +1056,18 @@ export function createRSCHandler<
|
|
|
1014
1056
|
});
|
|
1015
1057
|
}
|
|
1016
1058
|
|
|
1017
|
-
// Delegate to SSR for HTML response
|
|
1018
|
-
const ssrModule = await
|
|
1019
|
-
|
|
1059
|
+
// Delegate to SSR for HTML response (reuse early setup if available)
|
|
1060
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
1061
|
+
handlerCtx,
|
|
1062
|
+
request,
|
|
1063
|
+
env,
|
|
1064
|
+
url,
|
|
1065
|
+
requireRequestContext()._metricsStore,
|
|
1066
|
+
);
|
|
1067
|
+
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
1068
|
+
nonce,
|
|
1069
|
+
streamMode,
|
|
1070
|
+
});
|
|
1020
1071
|
|
|
1021
1072
|
return createResponseWithMergedHeaders(htmlStream, {
|
|
1022
1073
|
status: 404,
|