@rangojs/router 0.0.0-experimental.fa8a383a → 0.0.0-experimental.fb4fdc18
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 +188 -35
- package/dist/bin/rango.js +130 -47
- package/dist/vite/index.js +1884 -537
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +7 -5
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +8 -0
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +33 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +93 -17
- package/skills/loader/SKILL.md +123 -46
- package/skills/middleware/SKILL.md +36 -3
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/parallel/SKILL.md +133 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/rango/SKILL.md +26 -22
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +75 -0
- package/skills/router-setup/SKILL.md +87 -2
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +19 -1
- package/src/__internal.ts +1 -1
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +44 -4
- package/src/browser/navigation-bridge.ts +95 -7
- package/src/browser/navigation-client.ts +128 -53
- package/src/browser/navigation-store.ts +68 -9
- package/src/browser/partial-update.ts +93 -12
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +156 -18
- package/src/browser/prefetch/queue.ts +92 -29
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +72 -8
- package/src/browser/react/NavigationProvider.tsx +82 -21
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +17 -4
- package/src/browser/react/use-router.ts +29 -9
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/rsc-router.tsx +60 -9
- package/src/browser/scroll-restoration.ts +10 -8
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +46 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +211 -72
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +46 -5
- package/src/cache/cf/cf-cache-store.ts +5 -7
- package/src/cache/taint.ts +55 -0
- package/src/client.tsx +84 -230
- package/src/context-var.ts +72 -2
- package/src/handle.ts +40 -0
- package/src/index.rsc.ts +6 -1
- package/src/index.ts +49 -6
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +28 -2
- package/src/route-definition/dsl-helpers.ts +210 -35
- package/src/route-definition/helpers-types.ts +73 -20
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +9 -1
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/handler-context.ts +102 -25
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/lazy-includes.ts +6 -6
- package/src/router/loader-resolution.ts +159 -21
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +128 -192
- package/src/router/match-handlers.ts +1 -0
- package/src/router/match-middleware/background-revalidation.ts +12 -1
- package/src/router/match-middleware/cache-lookup.ts +74 -14
- package/src/router/match-middleware/cache-store.ts +21 -4
- package/src/router/match-middleware/segment-resolution.ts +53 -0
- package/src/router/match-result.ts +112 -9
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware-types.ts +20 -33
- package/src/router/middleware.ts +56 -12
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +15 -1
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +114 -18
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/revalidation.ts +257 -127
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +1 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +55 -7
- package/src/rsc/handler.ts +478 -383
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +18 -2
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +20 -1
- package/src/rsc/server-action.ts +12 -0
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +15 -1
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +22 -62
- package/src/server/context.ts +76 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +185 -57
- package/src/ssr/index.tsx +8 -1
- package/src/static-handler.ts +18 -6
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +145 -68
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +12 -1
- package/src/types/segments.ts +18 -1
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +47 -12
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +18 -16
- package/src/use-loader.tsx +77 -5
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/discover-routers.ts +36 -4
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +175 -74
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +13 -4
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +60 -5
- package/src/vite/plugins/cjs-to-esm.ts +5 -0
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +16 -4
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +52 -28
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +563 -316
- package/src/vite/plugins/performance-tracks.ts +96 -0
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/plugins/use-cache-transform.ts +56 -43
- package/src/vite/plugins/version-injector.ts +37 -11
- package/src/vite/rango.ts +63 -11
- package/src/vite/router-discovery.ts +732 -86
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +38 -5
- package/src/vite/utils/shared-utils.ts +3 -2
package/src/rsc/handler.ts
CHANGED
|
@@ -14,10 +14,11 @@ import {
|
|
|
14
14
|
runWithRequestContext,
|
|
15
15
|
setRequestContextParams,
|
|
16
16
|
requireRequestContext,
|
|
17
|
+
getRequestContext,
|
|
18
|
+
_getRequestContext,
|
|
17
19
|
createRequestContext,
|
|
18
20
|
} from "../server/request-context.js";
|
|
19
21
|
import * as rscDeps from "@vitejs/plugin-rsc/rsc";
|
|
20
|
-
|
|
21
22
|
import type {
|
|
22
23
|
RscPayload,
|
|
23
24
|
CreateRSCHandlerOptions,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
interceptRedirectForPartial,
|
|
31
32
|
buildRouteMiddlewareEntries,
|
|
32
33
|
} from "./helpers.js";
|
|
34
|
+
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
33
35
|
import {
|
|
34
36
|
handleResponseRoute,
|
|
35
37
|
type ResponseRouteMatch,
|
|
@@ -55,6 +57,7 @@ import {
|
|
|
55
57
|
getRouterTrie,
|
|
56
58
|
} from "../route-map-builder.js";
|
|
57
59
|
import type { HandlerContext } from "./handler-context.js";
|
|
60
|
+
import type { SegmentCacheStore } from "../cache/types.js";
|
|
58
61
|
import { buildRouterTrieFromUrlpatterns } from "./manifest-init.js";
|
|
59
62
|
import { handleProgressiveEnhancement } from "./progressive-enhancement.js";
|
|
60
63
|
import {
|
|
@@ -82,6 +85,11 @@ import {
|
|
|
82
85
|
mayNeedSSR,
|
|
83
86
|
SSR_SETUP_VAR,
|
|
84
87
|
} from "./ssr-setup.js";
|
|
88
|
+
import {
|
|
89
|
+
classifyRequest,
|
|
90
|
+
type RequestPlan,
|
|
91
|
+
type ExecutableRequestPlan,
|
|
92
|
+
} from "../router/request-classification.js";
|
|
85
93
|
|
|
86
94
|
/**
|
|
87
95
|
* Create an RSC request handler.
|
|
@@ -161,10 +169,13 @@ export function createRSCHandler<
|
|
|
161
169
|
phase: ErrorPhase,
|
|
162
170
|
context: Parameters<typeof invokeOnError<TEnv>>[3],
|
|
163
171
|
): void {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
// Guard: abort signal handlers fire asynchronously outside the ALS
|
|
173
|
+
// request scope, so the context may be gone. Skip dedup in that
|
|
174
|
+
// case — the error is from a cancelled stream, not a real failure.
|
|
175
|
+
const reqCtx = _getRequestContext();
|
|
176
|
+
if (error != null && typeof error === "object" && reqCtx) {
|
|
177
|
+
if (reqCtx._reportedErrors.has(error)) return;
|
|
178
|
+
reqCtx._reportedErrors.add(error);
|
|
168
179
|
}
|
|
169
180
|
invokeOnError(router.onError, error, phase, context, "RSC");
|
|
170
181
|
}
|
|
@@ -343,7 +354,7 @@ export function createRSCHandler<
|
|
|
343
354
|
// Resolve cache store configuration
|
|
344
355
|
// Priority: options.cache (handler override) > router.cache (router default)
|
|
345
356
|
// Store is enabled only if: config provided, enabled, and no ?__no_cache query param
|
|
346
|
-
let cacheStore
|
|
357
|
+
let cacheStore: SegmentCacheStore | undefined;
|
|
347
358
|
const cacheOption = options.cache ?? router.cache;
|
|
348
359
|
if (cacheOption && !url.searchParams.has("__no_cache")) {
|
|
349
360
|
const cacheConfig =
|
|
@@ -452,6 +463,9 @@ export function createRSCHandler<
|
|
|
452
463
|
// - Server components during rendering
|
|
453
464
|
// - Error boundaries
|
|
454
465
|
// - Streaming
|
|
466
|
+
// Store basename on request context (scoped per-request via existing ALS)
|
|
467
|
+
requestContext._basename = router.basename;
|
|
468
|
+
|
|
455
469
|
return runWithRequestContext(requestContext, async () => {
|
|
456
470
|
// Core handler logic (wrapped by middleware)
|
|
457
471
|
const coreHandler = async (): Promise<Response> => {
|
|
@@ -490,7 +504,6 @@ export function createRSCHandler<
|
|
|
490
504
|
// has completed so :post spans are captured in the timeline.
|
|
491
505
|
// Handler timing parts are always emitted (even without debug metrics)
|
|
492
506
|
// so non-debug requests still get bootstrap Server-Timing entries.
|
|
493
|
-
const finalizeStart = performance.now();
|
|
494
507
|
const handlerTimingArr: string[] = variables.__handlerTiming || [];
|
|
495
508
|
// Preserve any existing Server-Timing set by response routes or middleware
|
|
496
509
|
const existingTiming = response.headers.get("Server-Timing");
|
|
@@ -507,14 +520,6 @@ export function createRSCHandler<
|
|
|
507
520
|
const totalStart = earlyMetricsStore
|
|
508
521
|
? handlerStart
|
|
509
522
|
: metricsStore.requestStart;
|
|
510
|
-
// response-finalize measures the gap between render completion and
|
|
511
|
-
// handler return: header assembly, onResponse callbacks, etc.
|
|
512
|
-
appendMetric(
|
|
513
|
-
metricsStore,
|
|
514
|
-
"response-finalize",
|
|
515
|
-
finalizeStart,
|
|
516
|
-
performance.now() - finalizeStart,
|
|
517
|
-
);
|
|
518
523
|
appendMetric(
|
|
519
524
|
metricsStore,
|
|
520
525
|
"handler:total",
|
|
@@ -530,13 +535,17 @@ export function createRSCHandler<
|
|
|
530
535
|
}
|
|
531
536
|
|
|
532
537
|
const fullTiming = timingParts.join(", ");
|
|
533
|
-
if (fullTiming
|
|
538
|
+
if (fullTiming && !isWebSocketUpgradeResponse(response)) {
|
|
539
|
+
response.headers.set("Server-Timing", fullTiming);
|
|
540
|
+
}
|
|
534
541
|
|
|
535
542
|
return response;
|
|
536
543
|
});
|
|
537
544
|
};
|
|
538
545
|
|
|
539
|
-
// Core request handling logic (separated for middleware wrapping)
|
|
546
|
+
// Core request handling logic (separated for middleware wrapping).
|
|
547
|
+
// Uses the classify → execute model: classifyRequest produces a RequestPlan,
|
|
548
|
+
// then execution dispatches on the plan mode.
|
|
540
549
|
async function coreRequestHandler(
|
|
541
550
|
request: Request,
|
|
542
551
|
env: TEnv,
|
|
@@ -544,71 +553,112 @@ export function createRSCHandler<
|
|
|
544
553
|
variables: Record<string, any>,
|
|
545
554
|
nonce: string | undefined,
|
|
546
555
|
): Promise<Response> {
|
|
547
|
-
const previewStart = performance.now();
|
|
548
|
-
const preview = await router.previewMatch(request, { env });
|
|
549
|
-
const previewDur = performance.now() - previewStart;
|
|
550
556
|
const handlerTiming: string[] = variables.__handlerTiming || [];
|
|
551
|
-
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
557
|
+
|
|
558
|
+
// Debug manifest endpoint: handled before classification since it
|
|
559
|
+
// doesn't need a route match and needs trie access from the closure.
|
|
560
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
561
|
+
if (
|
|
562
|
+
url.searchParams.has("__debug_manifest") &&
|
|
563
|
+
(isDev || router.allowDebugManifest)
|
|
564
|
+
) {
|
|
565
|
+
const trie = getRouterTrie(router.id) ?? getRouteTrie();
|
|
566
|
+
const routeManifest = getRequiredRouteMap();
|
|
567
|
+
const { extractAncestryFromTrie } =
|
|
568
|
+
await import("../build/route-trie.js");
|
|
569
|
+
return new Response(
|
|
570
|
+
JSON.stringify(
|
|
571
|
+
{
|
|
572
|
+
routerId: router.id,
|
|
573
|
+
routeManifest,
|
|
574
|
+
routeAncestry: trie ? extractAncestryFromTrie(trie) : {},
|
|
575
|
+
routeTrie: trie,
|
|
576
|
+
precomputedEntries: getPrecomputedEntries(),
|
|
577
|
+
},
|
|
578
|
+
null,
|
|
579
|
+
2,
|
|
562
580
|
),
|
|
563
|
-
|
|
564
|
-
|
|
581
|
+
{
|
|
582
|
+
headers: { "Content-Type": "application/json" },
|
|
583
|
+
},
|
|
565
584
|
);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ---- 1. Classify ----
|
|
588
|
+
// classifyRequest may throw RouteNotFoundError for unknown routes.
|
|
589
|
+
// In that case, fall through to a full-render plan so the pipeline
|
|
590
|
+
// can render the 404 page via the existing error handling path.
|
|
591
|
+
const classifyStart = performance.now();
|
|
592
|
+
let plan: RequestPlan<TEnv>;
|
|
593
|
+
try {
|
|
594
|
+
plan = await classifyRequest<TEnv>(request, url, {
|
|
595
|
+
findMatch: router.findMatch,
|
|
596
|
+
routerVersion: version,
|
|
597
|
+
routerId: router.id,
|
|
598
|
+
});
|
|
599
|
+
} catch (error) {
|
|
600
|
+
if (
|
|
601
|
+
error instanceof RouteNotFoundError ||
|
|
602
|
+
(error instanceof Error && error.name === "RouteNotFoundError")
|
|
603
|
+
) {
|
|
604
|
+
// Let the render path handle 404 — match()/matchPartial() will
|
|
605
|
+
// re-throw RouteNotFoundError and the catch block in
|
|
606
|
+
// executeRenderWithMiddleware renders the not-found page.
|
|
607
|
+
plan = {
|
|
608
|
+
mode: "full-render",
|
|
609
|
+
route: {
|
|
610
|
+
matched: null as any,
|
|
611
|
+
manifestEntry: null as any,
|
|
612
|
+
entries: [],
|
|
613
|
+
routeKey: "",
|
|
614
|
+
localRouteName: "",
|
|
615
|
+
params: {},
|
|
616
|
+
routeMiddleware: [],
|
|
617
|
+
cacheScope: null,
|
|
618
|
+
isPassthrough: false,
|
|
619
|
+
},
|
|
620
|
+
negotiated: false,
|
|
621
|
+
};
|
|
622
|
+
} else {
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const classifyDur = performance.now() - classifyStart;
|
|
627
|
+
handlerTiming.push(`handler-classify;dur=${classifyDur.toFixed(2)}`);
|
|
628
|
+
|
|
629
|
+
// ---- 2. Terminal plans (no execution needed) ----
|
|
630
|
+
if (plan.mode === "redirect") {
|
|
631
|
+
// Redirects are handled by the pipeline (match/matchPartial),
|
|
632
|
+
// but for partial requests we short-circuit with a Flight redirect.
|
|
633
|
+
if (url.searchParams.has("_rsc_partial")) {
|
|
634
|
+
return createRedirectFlightResponse(plan.redirectUrl);
|
|
575
635
|
}
|
|
576
|
-
|
|
636
|
+
// Full requests: let the pipeline handle the redirect via match()
|
|
637
|
+
// which returns { redirect: url }. Fall through to full-render.
|
|
577
638
|
}
|
|
578
639
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (mayNeedSSR(request, url)) {
|
|
583
|
-
variables[SSR_SETUP_VAR] = startSSRSetup(
|
|
584
|
-
handlerCtx,
|
|
585
|
-
request,
|
|
586
|
-
env,
|
|
587
|
-
url,
|
|
588
|
-
router.debugPerformance
|
|
589
|
-
? () => requireRequestContext()._metricsStore
|
|
590
|
-
: undefined,
|
|
640
|
+
if (plan.mode === "version-mismatch") {
|
|
641
|
+
console.log(
|
|
642
|
+
`[RSC] Version mismatch: client=${url.searchParams.get("_rsc_v")}, server=${version}. Forcing reload.`,
|
|
591
643
|
);
|
|
644
|
+
return createResponseWithMergedHeaders(null, {
|
|
645
|
+
status: 200,
|
|
646
|
+
headers: {
|
|
647
|
+
"X-RSC-Reload": plan.reloadUrl,
|
|
648
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
649
|
+
},
|
|
650
|
+
});
|
|
592
651
|
}
|
|
593
652
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
// PE form submissions before any execution. Regular page navigations
|
|
604
|
-
// (GET without _rsc_loader/_rsc_action) are not affected.
|
|
605
|
-
const originPhase: OriginCheckPhase | null = isAction
|
|
606
|
-
? "action"
|
|
607
|
-
: isLoaderFetch
|
|
608
|
-
? "loader"
|
|
609
|
-
: request.method === "POST"
|
|
610
|
-
? "pe-form"
|
|
611
|
-
: null;
|
|
653
|
+
// ---- 3. Origin guard (gate for action/loader/PE modes) ----
|
|
654
|
+
const originPhase: OriginCheckPhase | null =
|
|
655
|
+
plan.mode === "action"
|
|
656
|
+
? "action"
|
|
657
|
+
: plan.mode === "loader"
|
|
658
|
+
? "loader"
|
|
659
|
+
: plan.mode === "pe-render"
|
|
660
|
+
? "pe-form"
|
|
661
|
+
: null;
|
|
612
662
|
if (originPhase) {
|
|
613
663
|
const originResult = await checkRequestOrigin(
|
|
614
664
|
request,
|
|
@@ -658,13 +708,33 @@ export function createRSCHandler<
|
|
|
658
708
|
}
|
|
659
709
|
}
|
|
660
710
|
|
|
661
|
-
//
|
|
711
|
+
// ---- 4. Execute ----
|
|
712
|
+
return executeRequest(
|
|
713
|
+
plan as ExecutableRequestPlan<TEnv>,
|
|
714
|
+
request,
|
|
715
|
+
env,
|
|
716
|
+
url,
|
|
717
|
+
variables,
|
|
718
|
+
nonce,
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Execute a classified request plan. Dispatches to the appropriate handler
|
|
723
|
+
// based on plan.mode. Lives in the createRSCHandler closure for access to
|
|
724
|
+
// handlerCtx, router, callOnError, etc.
|
|
725
|
+
// Only receives executable plans (version-mismatch is handled above).
|
|
726
|
+
async function executeRequest(
|
|
727
|
+
plan: ExecutableRequestPlan<TEnv>,
|
|
728
|
+
request: Request,
|
|
729
|
+
env: TEnv,
|
|
730
|
+
url: URL,
|
|
731
|
+
variables: Record<string, any>,
|
|
732
|
+
nonce: string | undefined,
|
|
733
|
+
): Promise<Response> {
|
|
734
|
+
// Common setup
|
|
662
735
|
const handleStore = requireRequestContext()._handleStore;
|
|
663
736
|
|
|
664
737
|
// Wire up error reporting for late streaming-handle failures
|
|
665
|
-
// (LateHandlePushError: handle pushed after stream completion).
|
|
666
|
-
// Without this, these errors are only caught by React's error boundary
|
|
667
|
-
// and never reach the router's onError callback or telemetry.
|
|
668
738
|
handleStore.onError = (error: Error) => {
|
|
669
739
|
const reqCtx = requireRequestContext();
|
|
670
740
|
callOnError(error, "handler", {
|
|
@@ -694,37 +764,106 @@ export function createRSCHandler<
|
|
|
694
764
|
};
|
|
695
765
|
|
|
696
766
|
// Set route params early so all execution paths can access ctx.params.
|
|
697
|
-
|
|
698
|
-
|
|
767
|
+
// Also store the classified snapshot so match/matchPartial can reuse it
|
|
768
|
+
// instead of calling resolveRoute again.
|
|
769
|
+
if (plan.mode !== "redirect") {
|
|
770
|
+
setRequestContextParams(plan.route.params, plan.route.routeKey);
|
|
771
|
+
requireRequestContext()._classifiedRoute = plan.route;
|
|
699
772
|
}
|
|
700
773
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
//
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
routeMiddleware:
|
|
774
|
+
const routeReverse = createReverseFunction(getRequiredRouteMap());
|
|
775
|
+
|
|
776
|
+
// ---- Response route: skip entire RSC pipeline ----
|
|
777
|
+
if (plan.mode === "response") {
|
|
778
|
+
// Build ResponseRouteMatch from plan fields. handleResponseRoute
|
|
779
|
+
// expects a flat object with params at the top level.
|
|
780
|
+
const responseMatch: ResponseRouteMatch = {
|
|
781
|
+
responseType: plan.responseType,
|
|
782
|
+
handler: plan.handler,
|
|
783
|
+
params: plan.route.params,
|
|
784
|
+
negotiated: plan.negotiated,
|
|
785
|
+
manifestEntry: plan.manifestEntry,
|
|
786
|
+
routeMiddleware: plan.routeMiddleware,
|
|
787
|
+
};
|
|
788
|
+
const responseOutcome = await withTimeout(
|
|
789
|
+
handleResponseRoute(
|
|
790
|
+
handlerCtx,
|
|
791
|
+
responseMatch,
|
|
792
|
+
request,
|
|
793
|
+
env,
|
|
794
|
+
url,
|
|
795
|
+
variables,
|
|
796
|
+
),
|
|
797
|
+
router.timeouts.renderStartMs,
|
|
798
|
+
"render-start",
|
|
799
|
+
);
|
|
800
|
+
if (responseOutcome.timedOut) {
|
|
801
|
+
return handleTimeoutResponse(
|
|
802
|
+
request,
|
|
803
|
+
env,
|
|
804
|
+
url,
|
|
805
|
+
"render-start",
|
|
806
|
+
responseOutcome.durationMs,
|
|
807
|
+
plan.route.routeKey,
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
const response = responseOutcome.result;
|
|
811
|
+
if (plan.negotiated && !isWebSocketUpgradeResponse(response)) {
|
|
812
|
+
response.headers.append("Vary", "Accept");
|
|
813
|
+
}
|
|
814
|
+
return response;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// SSR setup: kick off in parallel for modes that need HTML rendering.
|
|
818
|
+
// Placed after response-route short-circuit so response/mime routes
|
|
819
|
+
// never pay for SSR work.
|
|
820
|
+
if (plan.mode !== "loader" && mayNeedSSR(request, url)) {
|
|
821
|
+
variables[SSR_SETUP_VAR] = startSSRSetup(
|
|
822
|
+
handlerCtx,
|
|
823
|
+
request,
|
|
824
|
+
env,
|
|
825
|
+
url,
|
|
826
|
+
router.debugPerformance
|
|
827
|
+
? () => requireRequestContext()._metricsStore
|
|
828
|
+
: undefined,
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// ---- Loader fetch ----
|
|
833
|
+
if (plan.mode === "loader") {
|
|
834
|
+
return handleLoaderFetch(
|
|
835
|
+
handlerCtx,
|
|
836
|
+
request,
|
|
837
|
+
env,
|
|
838
|
+
url,
|
|
714
839
|
variables,
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
);
|
|
718
|
-
if (progressiveResult) {
|
|
719
|
-
return progressiveResult;
|
|
840
|
+
plan.route.params,
|
|
841
|
+
);
|
|
720
842
|
}
|
|
721
843
|
|
|
722
|
-
//
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
844
|
+
// ---- Progressive enhancement ----
|
|
845
|
+
if (plan.mode === "pe-render") {
|
|
846
|
+
const peResult = await handleProgressiveEnhancement(
|
|
847
|
+
handlerCtx,
|
|
848
|
+
request,
|
|
849
|
+
env,
|
|
850
|
+
url,
|
|
851
|
+
false, // isAction = false for PE
|
|
852
|
+
handleStore,
|
|
853
|
+
nonce,
|
|
854
|
+
{
|
|
855
|
+
routeMiddleware: plan.route.routeMiddleware,
|
|
856
|
+
variables,
|
|
857
|
+
routeReverse,
|
|
858
|
+
},
|
|
859
|
+
);
|
|
860
|
+
if (peResult) return peResult;
|
|
861
|
+
// PE handler returned null (not a PE form) — fall through to render
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// ---- Action: execute action, then revalidate wrapped in route middleware ----
|
|
865
|
+
if (plan.mode === "action") {
|
|
866
|
+
let actionContinuation: ActionContinuation | undefined;
|
|
728
867
|
try {
|
|
729
868
|
const actionOutcome = await withTimeout(
|
|
730
869
|
executeServerAction(
|
|
@@ -732,7 +871,7 @@ export function createRSCHandler<
|
|
|
732
871
|
request,
|
|
733
872
|
env,
|
|
734
873
|
url,
|
|
735
|
-
actionId,
|
|
874
|
+
plan.actionId,
|
|
736
875
|
handleStore,
|
|
737
876
|
),
|
|
738
877
|
router.timeouts.actionMs,
|
|
@@ -745,8 +884,8 @@ export function createRSCHandler<
|
|
|
745
884
|
url,
|
|
746
885
|
"action",
|
|
747
886
|
actionOutcome.durationMs,
|
|
748
|
-
|
|
749
|
-
actionId,
|
|
887
|
+
plan.route.routeKey,
|
|
888
|
+
plan.actionId,
|
|
750
889
|
);
|
|
751
890
|
}
|
|
752
891
|
const result = actionOutcome.result;
|
|
@@ -758,341 +897,297 @@ export function createRSCHandler<
|
|
|
758
897
|
request,
|
|
759
898
|
url,
|
|
760
899
|
env,
|
|
761
|
-
actionId,
|
|
900
|
+
actionId: plan.actionId,
|
|
762
901
|
handledByBoundary: false,
|
|
763
902
|
});
|
|
764
903
|
console.error(`[RSC] Action error:`, error);
|
|
765
904
|
throw error;
|
|
766
905
|
}
|
|
767
|
-
}
|
|
768
906
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
907
|
+
// Revalidation render wrapped in route middleware.
|
|
908
|
+
// Actions from client-side navigation include _rsc_partial — preserve
|
|
909
|
+
// the partial flag so the revalidation returns a Flight stream, not HTML.
|
|
910
|
+
// App-switch is already excluded by classifyRequest (would be full-render).
|
|
911
|
+
const isPartialAction = url.searchParams.has("_rsc_partial");
|
|
912
|
+
return executeRenderWithMiddleware(
|
|
913
|
+
plan.route.routeMiddleware,
|
|
914
|
+
plan.negotiated,
|
|
915
|
+
plan.route.routeKey,
|
|
916
|
+
routeReverse,
|
|
773
917
|
request,
|
|
774
918
|
env,
|
|
775
919
|
url,
|
|
776
920
|
variables,
|
|
777
921
|
nonce,
|
|
778
|
-
preview?.params,
|
|
779
|
-
preview?.routeKey,
|
|
780
922
|
handleStore,
|
|
923
|
+
isPartialAction,
|
|
781
924
|
actionContinuation,
|
|
782
925
|
);
|
|
783
|
-
|
|
784
|
-
response.headers.append("Vary", "Accept");
|
|
785
|
-
}
|
|
786
|
-
return response;
|
|
787
|
-
};
|
|
788
|
-
|
|
789
|
-
// Wrap the render path (with or without route middleware) in a
|
|
790
|
-
// renderStartMs timeout so slow renders are caught before output.
|
|
791
|
-
const executeRender = async (): Promise<Response> => {
|
|
792
|
-
if (preview?.routeMiddleware && preview.routeMiddleware.length > 0) {
|
|
793
|
-
const mwResponse = await executeMiddleware(
|
|
794
|
-
buildRouteMiddlewareEntries<TEnv>(preview.routeMiddleware),
|
|
795
|
-
request,
|
|
796
|
-
env,
|
|
797
|
-
variables,
|
|
798
|
-
renderHandler,
|
|
799
|
-
routeReverse,
|
|
800
|
-
);
|
|
801
|
-
|
|
802
|
-
if (
|
|
803
|
-
url.searchParams.has("_rsc_partial") ||
|
|
804
|
-
url.searchParams.has("_rsc_action")
|
|
805
|
-
) {
|
|
806
|
-
const intercepted = interceptRedirectForPartial(
|
|
807
|
-
mwResponse,
|
|
808
|
-
createRedirectFlightResponse,
|
|
809
|
-
);
|
|
810
|
-
if (intercepted) return intercepted;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
return finalizeResponse(mwResponse);
|
|
814
|
-
}
|
|
926
|
+
}
|
|
815
927
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
928
|
+
// ---- Full render / Partial render (or PE that fell through) ----
|
|
929
|
+
if (plan.mode === "full-render" || plan.mode === "partial-render") {
|
|
930
|
+
const isPartial = plan.mode === "partial-render";
|
|
931
|
+
return executeRenderWithMiddleware(
|
|
932
|
+
plan.route.routeMiddleware,
|
|
933
|
+
plan.negotiated,
|
|
934
|
+
plan.route.routeKey,
|
|
935
|
+
routeReverse,
|
|
936
|
+
request,
|
|
937
|
+
env,
|
|
938
|
+
url,
|
|
939
|
+
variables,
|
|
940
|
+
nonce,
|
|
941
|
+
handleStore,
|
|
942
|
+
isPartial,
|
|
943
|
+
);
|
|
944
|
+
}
|
|
819
945
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
946
|
+
// PE that fell through (handleProgressiveEnhancement returned null)
|
|
947
|
+
// falls back to full render
|
|
948
|
+
if (plan.mode === "pe-render") {
|
|
949
|
+
return executeRenderWithMiddleware(
|
|
950
|
+
plan.route.routeMiddleware,
|
|
951
|
+
false,
|
|
952
|
+
plan.route.routeKey,
|
|
953
|
+
routeReverse,
|
|
827
954
|
request,
|
|
828
955
|
env,
|
|
829
956
|
url,
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
957
|
+
variables,
|
|
958
|
+
nonce,
|
|
959
|
+
handleStore,
|
|
960
|
+
false,
|
|
833
961
|
);
|
|
834
962
|
}
|
|
835
|
-
|
|
963
|
+
|
|
964
|
+
// Redirect plan that wasn't handled above (full-page redirect — let
|
|
965
|
+
// the pipeline handle it via match() which returns { redirect: url })
|
|
966
|
+
return executeRenderWithMiddleware(
|
|
967
|
+
plan.route.routeMiddleware,
|
|
968
|
+
false,
|
|
969
|
+
plan.route.routeKey,
|
|
970
|
+
routeReverse,
|
|
971
|
+
request,
|
|
972
|
+
env,
|
|
973
|
+
url,
|
|
974
|
+
variables,
|
|
975
|
+
nonce,
|
|
976
|
+
handleStore,
|
|
977
|
+
false,
|
|
978
|
+
);
|
|
836
979
|
}
|
|
837
980
|
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
async function
|
|
981
|
+
// Shared render execution: wraps handleRscRendering (or revalidateAfterAction)
|
|
982
|
+
// in route middleware and timeout handling. Consolidates the pattern used by
|
|
983
|
+
// action-revalidate, full-render, and partial-render modes.
|
|
984
|
+
async function executeRenderWithMiddleware(
|
|
985
|
+
routeMiddleware: import("../router/middleware-types.js").CollectedMiddleware[],
|
|
986
|
+
negotiated: boolean,
|
|
987
|
+
routeKey: string,
|
|
988
|
+
routeReverse: ReturnType<typeof createReverseFunction>,
|
|
842
989
|
request: Request,
|
|
843
990
|
env: TEnv,
|
|
844
991
|
url: URL,
|
|
845
992
|
variables: Record<string, any>,
|
|
846
993
|
nonce: string | undefined,
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
handleStore?: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
994
|
+
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
995
|
+
isPartial: boolean,
|
|
850
996
|
actionContinuation?: ActionContinuation,
|
|
851
997
|
): Promise<Response> {
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
998
|
+
const renderHandler = async (): Promise<Response> => {
|
|
999
|
+
try {
|
|
1000
|
+
let response: Response;
|
|
1001
|
+
if (actionContinuation) {
|
|
1002
|
+
response = await revalidateAfterAction(
|
|
1003
|
+
handlerCtx,
|
|
1004
|
+
request,
|
|
1005
|
+
env,
|
|
1006
|
+
url,
|
|
1007
|
+
handleStore,
|
|
1008
|
+
actionContinuation,
|
|
1009
|
+
);
|
|
1010
|
+
} else {
|
|
1011
|
+
response = await handleRscRendering(
|
|
1012
|
+
handlerCtx,
|
|
1013
|
+
request,
|
|
1014
|
+
env,
|
|
1015
|
+
url,
|
|
1016
|
+
isPartial,
|
|
1017
|
+
handleStore,
|
|
1018
|
+
nonce,
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
if (negotiated && !isWebSocketUpgradeResponse(response)) {
|
|
1022
|
+
response.headers.append("Vary", "Accept");
|
|
1023
|
+
}
|
|
1024
|
+
return response;
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
// Check if middleware/handler returned Response
|
|
1027
|
+
if (error instanceof Response) {
|
|
1028
|
+
// During partial (client-side navigation), a 200 Response from a handler
|
|
1029
|
+
// means the route serves raw content (JSON, text, etc.), not JSX.
|
|
1030
|
+
// Signal the browser to hard-navigate so it renders the raw response.
|
|
1031
|
+
if (isPartial && error.status === 200) {
|
|
1032
|
+
console.warn(
|
|
1033
|
+
`[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
|
|
1034
|
+
`Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
|
|
1035
|
+
);
|
|
1036
|
+
return createResponseWithMergedHeaders(null, {
|
|
1037
|
+
status: 200,
|
|
1038
|
+
headers: {
|
|
1039
|
+
"X-RSC-Reload": stripInternalParams(url).toString(),
|
|
1040
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
863
1044
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
if (referer) {
|
|
871
|
-
try {
|
|
872
|
-
const refererUrl = new URL(referer);
|
|
873
|
-
if (refererUrl.origin === url.origin) {
|
|
874
|
-
reloadUrl = referer;
|
|
875
|
-
}
|
|
876
|
-
} catch {
|
|
877
|
-
// Malformed referer, fall back to cleanUrl
|
|
1045
|
+
if (isPartial) {
|
|
1046
|
+
const intercepted = interceptRedirectForPartial(
|
|
1047
|
+
error,
|
|
1048
|
+
createRedirectFlightResponse,
|
|
1049
|
+
);
|
|
1050
|
+
if (intercepted) return intercepted;
|
|
878
1051
|
}
|
|
1052
|
+
|
|
1053
|
+
return error;
|
|
879
1054
|
}
|
|
880
|
-
}
|
|
881
1055
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
894
|
-
if (
|
|
895
|
-
url.searchParams.has("__debug_manifest") &&
|
|
896
|
-
(isDev || router.allowDebugManifest)
|
|
897
|
-
) {
|
|
898
|
-
const trie = getRouterTrie(router.id) ?? getRouteTrie();
|
|
899
|
-
const routeManifest = getRequiredRouteMap();
|
|
900
|
-
const { extractAncestryFromTrie } =
|
|
901
|
-
await import("../build/route-trie.js");
|
|
902
|
-
return new Response(
|
|
903
|
-
JSON.stringify(
|
|
904
|
-
{
|
|
905
|
-
routerId: router.id,
|
|
906
|
-
routeManifest,
|
|
907
|
-
routeAncestry: trie ? extractAncestryFromTrie(trie) : {},
|
|
908
|
-
routeTrie: trie,
|
|
909
|
-
precomputedEntries: getPrecomputedEntries(),
|
|
910
|
-
},
|
|
911
|
-
null,
|
|
912
|
-
2,
|
|
913
|
-
),
|
|
914
|
-
{
|
|
915
|
-
headers: { "Content-Type": "application/json" },
|
|
916
|
-
},
|
|
917
|
-
);
|
|
918
|
-
}
|
|
1056
|
+
// Render 404 page for unmatched routes
|
|
1057
|
+
const isRouteNotFound =
|
|
1058
|
+
error instanceof RouteNotFoundError ||
|
|
1059
|
+
(error instanceof Error && error.name === "RouteNotFoundError");
|
|
1060
|
+
if (isRouteNotFound) {
|
|
1061
|
+
callOnError(error, "routing", {
|
|
1062
|
+
request,
|
|
1063
|
+
url,
|
|
1064
|
+
env,
|
|
1065
|
+
handledByBoundary: true,
|
|
1066
|
+
});
|
|
919
1067
|
|
|
920
|
-
|
|
1068
|
+
const notFoundOption = router.notFound;
|
|
1069
|
+
const notFoundComponent =
|
|
1070
|
+
typeof notFoundOption === "function"
|
|
1071
|
+
? notFoundOption({ pathname: url.pathname })
|
|
1072
|
+
: (notFoundOption ?? createElement("h1", null, "Not Found"));
|
|
1073
|
+
|
|
1074
|
+
const notFoundSegment = {
|
|
1075
|
+
id: "notFound",
|
|
1076
|
+
namespace: "notFound",
|
|
1077
|
+
type: "route" as const,
|
|
1078
|
+
index: 0,
|
|
1079
|
+
component: notFoundComponent,
|
|
1080
|
+
params: {},
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
const payload: RscPayload = {
|
|
1084
|
+
metadata: {
|
|
1085
|
+
pathname: url.pathname,
|
|
1086
|
+
routerId: router.id,
|
|
1087
|
+
basename: router.basename,
|
|
1088
|
+
segments: [notFoundSegment],
|
|
1089
|
+
matched: [],
|
|
1090
|
+
diff: [],
|
|
1091
|
+
isPartial: false,
|
|
1092
|
+
rootLayout: router.rootLayout,
|
|
1093
|
+
handles: handleStore.stream(),
|
|
1094
|
+
version,
|
|
1095
|
+
themeConfig: router.themeConfig,
|
|
1096
|
+
warmupEnabled: router.warmupEnabled,
|
|
1097
|
+
initialTheme: requireRequestContext().theme,
|
|
1098
|
+
},
|
|
1099
|
+
};
|
|
921
1100
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
}
|
|
1101
|
+
const rscStream = renderToReadableStream(payload, {
|
|
1102
|
+
onError: (error: unknown) => {
|
|
1103
|
+
callOnError(error, "rendering", { request, url, env });
|
|
1104
|
+
},
|
|
1105
|
+
});
|
|
928
1106
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
handlerCtx,
|
|
935
|
-
request,
|
|
936
|
-
env,
|
|
937
|
-
url,
|
|
938
|
-
store,
|
|
939
|
-
actionContinuation,
|
|
940
|
-
);
|
|
941
|
-
}
|
|
1107
|
+
const isRscRequest =
|
|
1108
|
+
isPartial ||
|
|
1109
|
+
(!request.headers.get("accept")?.includes("text/html") &&
|
|
1110
|
+
!url.searchParams.has("__html")) ||
|
|
1111
|
+
url.searchParams.has("__rsc");
|
|
942
1112
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
handlerCtx,
|
|
950
|
-
request,
|
|
951
|
-
env,
|
|
952
|
-
url,
|
|
953
|
-
variables,
|
|
954
|
-
routeParams,
|
|
955
|
-
);
|
|
956
|
-
}
|
|
1113
|
+
if (isRscRequest) {
|
|
1114
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
1115
|
+
status: 404,
|
|
1116
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
957
1119
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
request,
|
|
965
|
-
env,
|
|
966
|
-
url,
|
|
967
|
-
isPartial,
|
|
968
|
-
store,
|
|
969
|
-
nonce,
|
|
970
|
-
);
|
|
971
|
-
} catch (error) {
|
|
972
|
-
// Check if middleware/handler returned Response
|
|
973
|
-
if (error instanceof Response) {
|
|
974
|
-
// During partial (client-side navigation), a 200 Response from a handler
|
|
975
|
-
// means the route serves raw content (JSON, text, etc.), not JSX.
|
|
976
|
-
// Signal the browser to hard-navigate so it renders the raw response.
|
|
977
|
-
// Only for 200 — redirects (3xx) work already because the browser follows
|
|
978
|
-
// them automatically to a URL that serves Flight data.
|
|
979
|
-
if (isPartial && error.status === 200) {
|
|
980
|
-
console.warn(
|
|
981
|
-
`[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
|
|
982
|
-
`Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
|
|
1120
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
1121
|
+
handlerCtx,
|
|
1122
|
+
request,
|
|
1123
|
+
env,
|
|
1124
|
+
url,
|
|
1125
|
+
requireRequestContext()._metricsStore,
|
|
983
1126
|
);
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
"X-RSC-Reload": stripInternalParams(url).toString(),
|
|
988
|
-
"content-type": "text/x-component;charset=utf-8",
|
|
989
|
-
},
|
|
1127
|
+
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
1128
|
+
nonce,
|
|
1129
|
+
streamMode,
|
|
990
1130
|
});
|
|
991
|
-
}
|
|
992
1131
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
);
|
|
998
|
-
if (intercepted) return intercepted;
|
|
1132
|
+
return createResponseWithMergedHeaders(htmlStream, {
|
|
1133
|
+
status: 404,
|
|
1134
|
+
headers: { "content-type": "text/html;charset=utf-8" },
|
|
1135
|
+
});
|
|
999
1136
|
}
|
|
1000
1137
|
|
|
1001
|
-
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// Render 404 page for unmatched routes
|
|
1005
|
-
// Check both instanceof and error.name for cross-bundle compatibility
|
|
1006
|
-
const isRouteNotFound =
|
|
1007
|
-
error instanceof RouteNotFoundError ||
|
|
1008
|
-
(error instanceof Error && error.name === "RouteNotFoundError");
|
|
1009
|
-
if (isRouteNotFound) {
|
|
1138
|
+
// Report unhandled errors
|
|
1010
1139
|
callOnError(error, "routing", {
|
|
1011
1140
|
request,
|
|
1012
1141
|
url,
|
|
1013
1142
|
env,
|
|
1014
|
-
handledByBoundary:
|
|
1143
|
+
handledByBoundary: false,
|
|
1015
1144
|
});
|
|
1145
|
+
console.error(`[RSC] Error:`, error);
|
|
1146
|
+
throw error;
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1016
1149
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
: (notFoundOption ?? createElement("h1", null, "Not Found"));
|
|
1023
|
-
|
|
1024
|
-
// Create a simple segment for the 404 page
|
|
1025
|
-
const notFoundSegment = {
|
|
1026
|
-
id: "notFound",
|
|
1027
|
-
namespace: "notFound",
|
|
1028
|
-
type: "route" as const,
|
|
1029
|
-
index: 0,
|
|
1030
|
-
component: notFoundComponent,
|
|
1031
|
-
params: {},
|
|
1032
|
-
};
|
|
1033
|
-
|
|
1034
|
-
const payload: RscPayload = {
|
|
1035
|
-
metadata: {
|
|
1036
|
-
pathname: url.pathname,
|
|
1037
|
-
segments: [notFoundSegment],
|
|
1038
|
-
matched: [],
|
|
1039
|
-
diff: [],
|
|
1040
|
-
isPartial: false,
|
|
1041
|
-
rootLayout: router.rootLayout,
|
|
1042
|
-
handles: store.stream(),
|
|
1043
|
-
version,
|
|
1044
|
-
themeConfig: router.themeConfig,
|
|
1045
|
-
warmupEnabled: router.warmupEnabled,
|
|
1046
|
-
initialTheme: requireRequestContext().theme,
|
|
1047
|
-
// No routeName for not-found routes
|
|
1048
|
-
},
|
|
1049
|
-
};
|
|
1050
|
-
|
|
1051
|
-
const rscStream = renderToReadableStream(payload);
|
|
1052
|
-
|
|
1053
|
-
// Determine if this is an RSC request or HTML request.
|
|
1054
|
-
// Partial requests are always RSC (see main isRscRequest comment).
|
|
1055
|
-
const isRscRequest =
|
|
1056
|
-
isPartial ||
|
|
1057
|
-
(!request.headers.get("accept")?.includes("text/html") &&
|
|
1058
|
-
!url.searchParams.has("__html")) ||
|
|
1059
|
-
url.searchParams.has("__rsc");
|
|
1060
|
-
|
|
1061
|
-
if (isRscRequest) {
|
|
1062
|
-
return createResponseWithMergedHeaders(rscStream, {
|
|
1063
|
-
status: 404,
|
|
1064
|
-
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
1065
|
-
});
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
// Delegate to SSR for HTML response (reuse early setup if available)
|
|
1069
|
-
const [ssrModule, streamMode] = await getSSRSetup(
|
|
1070
|
-
handlerCtx,
|
|
1150
|
+
// Wrap the render path in a renderStartMs timeout
|
|
1151
|
+
const executeRender = async (): Promise<Response> => {
|
|
1152
|
+
if (routeMiddleware.length > 0) {
|
|
1153
|
+
const mwResponse = await executeMiddleware(
|
|
1154
|
+
buildRouteMiddlewareEntries<TEnv>(routeMiddleware),
|
|
1071
1155
|
request,
|
|
1072
1156
|
env,
|
|
1073
|
-
|
|
1074
|
-
|
|
1157
|
+
variables,
|
|
1158
|
+
renderHandler,
|
|
1159
|
+
routeReverse,
|
|
1075
1160
|
);
|
|
1076
|
-
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
1077
|
-
nonce,
|
|
1078
|
-
streamMode,
|
|
1079
|
-
});
|
|
1080
1161
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1162
|
+
if (isPartial || actionContinuation) {
|
|
1163
|
+
const intercepted = interceptRedirectForPartial(
|
|
1164
|
+
mwResponse,
|
|
1165
|
+
createRedirectFlightResponse,
|
|
1166
|
+
);
|
|
1167
|
+
if (intercepted) return intercepted;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
return finalizeResponse(mwResponse);
|
|
1085
1171
|
}
|
|
1086
1172
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1173
|
+
return renderHandler();
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
const renderOutcome = await withTimeout(
|
|
1177
|
+
executeRender(),
|
|
1178
|
+
router.timeouts.renderStartMs,
|
|
1179
|
+
"render-start",
|
|
1180
|
+
);
|
|
1181
|
+
if (renderOutcome.timedOut) {
|
|
1182
|
+
return handleTimeoutResponse(
|
|
1089
1183
|
request,
|
|
1090
|
-
url,
|
|
1091
1184
|
env,
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1185
|
+
url,
|
|
1186
|
+
"render-start",
|
|
1187
|
+
renderOutcome.durationMs,
|
|
1188
|
+
routeKey,
|
|
1189
|
+
);
|
|
1096
1190
|
}
|
|
1191
|
+
return renderOutcome.result;
|
|
1097
1192
|
}
|
|
1098
1193
|
}
|