@rangojs/router 0.0.0-experimental.63 → 0.0.0-experimental.64
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/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/src/client.tsx +2 -56
- package/src/route-definition/dsl-helpers.ts +5 -1
- package/src/route-definition/helpers-types.ts +4 -1
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/match-api.ts +124 -183
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/segment-resolution/fresh.ts +37 -0
- package/src/router/segment-resolution/revalidation.ts +43 -0
- package/src/router.ts +4 -0
- package/src/rsc/handler.ts +456 -373
- package/src/rsc/ssr-setup.ts +1 -1
- package/src/server/request-context.ts +7 -0
- package/src/urls/path-helper-types.ts +4 -1
- package/src/use-loader.tsx +73 -4
package/src/rsc/handler.ts
CHANGED
|
@@ -82,6 +82,11 @@ import {
|
|
|
82
82
|
mayNeedSSR,
|
|
83
83
|
SSR_SETUP_VAR,
|
|
84
84
|
} from "./ssr-setup.js";
|
|
85
|
+
import {
|
|
86
|
+
classifyRequest,
|
|
87
|
+
type RequestPlan,
|
|
88
|
+
type ExecutableRequestPlan,
|
|
89
|
+
} from "../router/request-classification.js";
|
|
85
90
|
|
|
86
91
|
/**
|
|
87
92
|
* Create an RSC request handler.
|
|
@@ -530,7 +535,9 @@ export function createRSCHandler<
|
|
|
530
535
|
});
|
|
531
536
|
};
|
|
532
537
|
|
|
533
|
-
// Core request handling logic (separated for middleware wrapping)
|
|
538
|
+
// Core request handling logic (separated for middleware wrapping).
|
|
539
|
+
// Uses the classify → execute model: classifyRequest produces a RequestPlan,
|
|
540
|
+
// then execution dispatches on the plan mode.
|
|
534
541
|
async function coreRequestHandler(
|
|
535
542
|
request: Request,
|
|
536
543
|
env: TEnv,
|
|
@@ -538,71 +545,112 @@ export function createRSCHandler<
|
|
|
538
545
|
variables: Record<string, any>,
|
|
539
546
|
nonce: string | undefined,
|
|
540
547
|
): Promise<Response> {
|
|
541
|
-
const previewStart = performance.now();
|
|
542
|
-
const preview = await router.previewMatch(request, { env });
|
|
543
|
-
const previewDur = performance.now() - previewStart;
|
|
544
548
|
const handlerTiming: string[] = variables.__handlerTiming || [];
|
|
545
|
-
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
549
|
+
|
|
550
|
+
// Debug manifest endpoint: handled before classification since it
|
|
551
|
+
// doesn't need a route match and needs trie access from the closure.
|
|
552
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
553
|
+
if (
|
|
554
|
+
url.searchParams.has("__debug_manifest") &&
|
|
555
|
+
(isDev || router.allowDebugManifest)
|
|
556
|
+
) {
|
|
557
|
+
const trie = getRouterTrie(router.id) ?? getRouteTrie();
|
|
558
|
+
const routeManifest = getRequiredRouteMap();
|
|
559
|
+
const { extractAncestryFromTrie } =
|
|
560
|
+
await import("../build/route-trie.js");
|
|
561
|
+
return new Response(
|
|
562
|
+
JSON.stringify(
|
|
563
|
+
{
|
|
564
|
+
routerId: router.id,
|
|
565
|
+
routeManifest,
|
|
566
|
+
routeAncestry: trie ? extractAncestryFromTrie(trie) : {},
|
|
567
|
+
routeTrie: trie,
|
|
568
|
+
precomputedEntries: getPrecomputedEntries(),
|
|
569
|
+
},
|
|
570
|
+
null,
|
|
571
|
+
2,
|
|
556
572
|
),
|
|
557
|
-
|
|
558
|
-
|
|
573
|
+
{
|
|
574
|
+
headers: { "Content-Type": "application/json" },
|
|
575
|
+
},
|
|
559
576
|
);
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ---- 1. Classify ----
|
|
580
|
+
// classifyRequest may throw RouteNotFoundError for unknown routes.
|
|
581
|
+
// In that case, fall through to a full-render plan so the pipeline
|
|
582
|
+
// can render the 404 page via the existing error handling path.
|
|
583
|
+
const classifyStart = performance.now();
|
|
584
|
+
let plan: RequestPlan<TEnv>;
|
|
585
|
+
try {
|
|
586
|
+
plan = await classifyRequest<TEnv>(request, url, {
|
|
587
|
+
findMatch: router.findMatch,
|
|
588
|
+
routerVersion: version,
|
|
589
|
+
routerId: router.id,
|
|
590
|
+
});
|
|
591
|
+
} catch (error) {
|
|
592
|
+
if (
|
|
593
|
+
error instanceof RouteNotFoundError ||
|
|
594
|
+
(error instanceof Error && error.name === "RouteNotFoundError")
|
|
595
|
+
) {
|
|
596
|
+
// Let the render path handle 404 — match()/matchPartial() will
|
|
597
|
+
// re-throw RouteNotFoundError and the catch block in
|
|
598
|
+
// executeRenderWithMiddleware renders the not-found page.
|
|
599
|
+
plan = {
|
|
600
|
+
mode: "full-render",
|
|
601
|
+
route: {
|
|
602
|
+
matched: null as any,
|
|
603
|
+
manifestEntry: null as any,
|
|
604
|
+
entries: [],
|
|
605
|
+
routeKey: "",
|
|
606
|
+
localRouteName: "",
|
|
607
|
+
params: {},
|
|
608
|
+
routeMiddleware: [],
|
|
609
|
+
cacheScope: null,
|
|
610
|
+
isPassthrough: false,
|
|
611
|
+
},
|
|
612
|
+
negotiated: false,
|
|
613
|
+
};
|
|
614
|
+
} else {
|
|
615
|
+
throw error;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const classifyDur = performance.now() - classifyStart;
|
|
619
|
+
handlerTiming.push(`handler-classify;dur=${classifyDur.toFixed(2)}`);
|
|
620
|
+
|
|
621
|
+
// ---- 2. Terminal plans (no execution needed) ----
|
|
622
|
+
if (plan.mode === "redirect") {
|
|
623
|
+
// Redirects are handled by the pipeline (match/matchPartial),
|
|
624
|
+
// but for partial requests we short-circuit with a Flight redirect.
|
|
625
|
+
if (url.searchParams.has("_rsc_partial")) {
|
|
626
|
+
return createRedirectFlightResponse(plan.redirectUrl);
|
|
569
627
|
}
|
|
570
|
-
|
|
628
|
+
// Full requests: let the pipeline handle the redirect via match()
|
|
629
|
+
// which returns { redirect: url }. Fall through to full-render.
|
|
571
630
|
}
|
|
572
631
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (mayNeedSSR(request, url)) {
|
|
577
|
-
variables[SSR_SETUP_VAR] = startSSRSetup(
|
|
578
|
-
handlerCtx,
|
|
579
|
-
request,
|
|
580
|
-
env,
|
|
581
|
-
url,
|
|
582
|
-
router.debugPerformance
|
|
583
|
-
? () => requireRequestContext()._metricsStore
|
|
584
|
-
: undefined,
|
|
632
|
+
if (plan.mode === "version-mismatch") {
|
|
633
|
+
console.log(
|
|
634
|
+
`[RSC] Version mismatch: client=${url.searchParams.get("_rsc_v")}, server=${version}. Forcing reload.`,
|
|
585
635
|
);
|
|
636
|
+
return createResponseWithMergedHeaders(null, {
|
|
637
|
+
status: 200,
|
|
638
|
+
headers: {
|
|
639
|
+
"X-RSC-Reload": plan.reloadUrl,
|
|
640
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
641
|
+
},
|
|
642
|
+
});
|
|
586
643
|
}
|
|
587
644
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
// PE form submissions before any execution. Regular page navigations
|
|
598
|
-
// (GET without _rsc_loader/_rsc_action) are not affected.
|
|
599
|
-
const originPhase: OriginCheckPhase | null = isAction
|
|
600
|
-
? "action"
|
|
601
|
-
: isLoaderFetch
|
|
602
|
-
? "loader"
|
|
603
|
-
: request.method === "POST"
|
|
604
|
-
? "pe-form"
|
|
605
|
-
: null;
|
|
645
|
+
// ---- 3. Origin guard (gate for action/loader/PE modes) ----
|
|
646
|
+
const originPhase: OriginCheckPhase | null =
|
|
647
|
+
plan.mode === "action"
|
|
648
|
+
? "action"
|
|
649
|
+
: plan.mode === "loader"
|
|
650
|
+
? "loader"
|
|
651
|
+
: plan.mode === "pe-render"
|
|
652
|
+
? "pe-form"
|
|
653
|
+
: null;
|
|
606
654
|
if (originPhase) {
|
|
607
655
|
const originResult = await checkRequestOrigin(
|
|
608
656
|
request,
|
|
@@ -652,13 +700,33 @@ export function createRSCHandler<
|
|
|
652
700
|
}
|
|
653
701
|
}
|
|
654
702
|
|
|
655
|
-
//
|
|
703
|
+
// ---- 4. Execute ----
|
|
704
|
+
return executeRequest(
|
|
705
|
+
plan as ExecutableRequestPlan<TEnv>,
|
|
706
|
+
request,
|
|
707
|
+
env,
|
|
708
|
+
url,
|
|
709
|
+
variables,
|
|
710
|
+
nonce,
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Execute a classified request plan. Dispatches to the appropriate handler
|
|
715
|
+
// based on plan.mode. Lives in the createRSCHandler closure for access to
|
|
716
|
+
// handlerCtx, router, callOnError, etc.
|
|
717
|
+
// Only receives executable plans (version-mismatch is handled above).
|
|
718
|
+
async function executeRequest(
|
|
719
|
+
plan: ExecutableRequestPlan<TEnv>,
|
|
720
|
+
request: Request,
|
|
721
|
+
env: TEnv,
|
|
722
|
+
url: URL,
|
|
723
|
+
variables: Record<string, any>,
|
|
724
|
+
nonce: string | undefined,
|
|
725
|
+
): Promise<Response> {
|
|
726
|
+
// Common setup
|
|
656
727
|
const handleStore = requireRequestContext()._handleStore;
|
|
657
728
|
|
|
658
729
|
// Wire up error reporting for late streaming-handle failures
|
|
659
|
-
// (LateHandlePushError: handle pushed after stream completion).
|
|
660
|
-
// Without this, these errors are only caught by React's error boundary
|
|
661
|
-
// and never reach the router's onError callback or telemetry.
|
|
662
730
|
handleStore.onError = (error: Error) => {
|
|
663
731
|
const reqCtx = requireRequestContext();
|
|
664
732
|
callOnError(error, "handler", {
|
|
@@ -688,37 +756,106 @@ export function createRSCHandler<
|
|
|
688
756
|
};
|
|
689
757
|
|
|
690
758
|
// Set route params early so all execution paths can access ctx.params.
|
|
691
|
-
|
|
692
|
-
|
|
759
|
+
// Also store the classified snapshot so match/matchPartial can reuse it
|
|
760
|
+
// instead of calling resolveRoute again.
|
|
761
|
+
if (plan.mode !== "redirect") {
|
|
762
|
+
setRequestContextParams(plan.route.params, plan.route.routeKey);
|
|
763
|
+
requireRequestContext()._classifiedRoute = plan.route;
|
|
693
764
|
}
|
|
694
765
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
//
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
routeMiddleware:
|
|
766
|
+
const routeReverse = createReverseFunction(getRequiredRouteMap());
|
|
767
|
+
|
|
768
|
+
// ---- Response route: skip entire RSC pipeline ----
|
|
769
|
+
if (plan.mode === "response") {
|
|
770
|
+
// Build ResponseRouteMatch from plan fields. handleResponseRoute
|
|
771
|
+
// expects a flat object with params at the top level.
|
|
772
|
+
const responseMatch: ResponseRouteMatch = {
|
|
773
|
+
responseType: plan.responseType,
|
|
774
|
+
handler: plan.handler,
|
|
775
|
+
params: plan.route.params,
|
|
776
|
+
negotiated: plan.negotiated,
|
|
777
|
+
manifestEntry: plan.manifestEntry,
|
|
778
|
+
routeMiddleware: plan.routeMiddleware,
|
|
779
|
+
};
|
|
780
|
+
const responseOutcome = await withTimeout(
|
|
781
|
+
handleResponseRoute(
|
|
782
|
+
handlerCtx,
|
|
783
|
+
responseMatch,
|
|
784
|
+
request,
|
|
785
|
+
env,
|
|
786
|
+
url,
|
|
787
|
+
variables,
|
|
788
|
+
),
|
|
789
|
+
router.timeouts.renderStartMs,
|
|
790
|
+
"render-start",
|
|
791
|
+
);
|
|
792
|
+
if (responseOutcome.timedOut) {
|
|
793
|
+
return handleTimeoutResponse(
|
|
794
|
+
request,
|
|
795
|
+
env,
|
|
796
|
+
url,
|
|
797
|
+
"render-start",
|
|
798
|
+
responseOutcome.durationMs,
|
|
799
|
+
plan.route.routeKey,
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
const response = responseOutcome.result;
|
|
803
|
+
if (plan.negotiated) {
|
|
804
|
+
response.headers.append("Vary", "Accept");
|
|
805
|
+
}
|
|
806
|
+
return response;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// SSR setup: kick off in parallel for modes that need HTML rendering.
|
|
810
|
+
// Placed after response-route short-circuit so response/mime routes
|
|
811
|
+
// never pay for SSR work.
|
|
812
|
+
if (plan.mode !== "loader" && mayNeedSSR(request, url)) {
|
|
813
|
+
variables[SSR_SETUP_VAR] = startSSRSetup(
|
|
814
|
+
handlerCtx,
|
|
815
|
+
request,
|
|
816
|
+
env,
|
|
817
|
+
url,
|
|
818
|
+
router.debugPerformance
|
|
819
|
+
? () => requireRequestContext()._metricsStore
|
|
820
|
+
: undefined,
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// ---- Loader fetch ----
|
|
825
|
+
if (plan.mode === "loader") {
|
|
826
|
+
return handleLoaderFetch(
|
|
827
|
+
handlerCtx,
|
|
828
|
+
request,
|
|
829
|
+
env,
|
|
830
|
+
url,
|
|
708
831
|
variables,
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
);
|
|
712
|
-
if (progressiveResult) {
|
|
713
|
-
return progressiveResult;
|
|
832
|
+
plan.route.params,
|
|
833
|
+
);
|
|
714
834
|
}
|
|
715
835
|
|
|
716
|
-
//
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
836
|
+
// ---- Progressive enhancement ----
|
|
837
|
+
if (plan.mode === "pe-render") {
|
|
838
|
+
const peResult = await handleProgressiveEnhancement(
|
|
839
|
+
handlerCtx,
|
|
840
|
+
request,
|
|
841
|
+
env,
|
|
842
|
+
url,
|
|
843
|
+
false, // isAction = false for PE
|
|
844
|
+
handleStore,
|
|
845
|
+
nonce,
|
|
846
|
+
{
|
|
847
|
+
routeMiddleware: plan.route.routeMiddleware,
|
|
848
|
+
variables,
|
|
849
|
+
routeReverse,
|
|
850
|
+
},
|
|
851
|
+
);
|
|
852
|
+
if (peResult) return peResult;
|
|
853
|
+
// PE handler returned null (not a PE form) — fall through to render
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ---- Action: execute action, then revalidate wrapped in route middleware ----
|
|
857
|
+
if (plan.mode === "action") {
|
|
858
|
+
let actionContinuation: ActionContinuation | undefined;
|
|
722
859
|
try {
|
|
723
860
|
const actionOutcome = await withTimeout(
|
|
724
861
|
executeServerAction(
|
|
@@ -726,7 +863,7 @@ export function createRSCHandler<
|
|
|
726
863
|
request,
|
|
727
864
|
env,
|
|
728
865
|
url,
|
|
729
|
-
actionId,
|
|
866
|
+
plan.actionId,
|
|
730
867
|
handleStore,
|
|
731
868
|
),
|
|
732
869
|
router.timeouts.actionMs,
|
|
@@ -739,8 +876,8 @@ export function createRSCHandler<
|
|
|
739
876
|
url,
|
|
740
877
|
"action",
|
|
741
878
|
actionOutcome.durationMs,
|
|
742
|
-
|
|
743
|
-
actionId,
|
|
879
|
+
plan.route.routeKey,
|
|
880
|
+
plan.actionId,
|
|
744
881
|
);
|
|
745
882
|
}
|
|
746
883
|
const result = actionOutcome.result;
|
|
@@ -752,347 +889,293 @@ export function createRSCHandler<
|
|
|
752
889
|
request,
|
|
753
890
|
url,
|
|
754
891
|
env,
|
|
755
|
-
actionId,
|
|
892
|
+
actionId: plan.actionId,
|
|
756
893
|
handledByBoundary: false,
|
|
757
894
|
});
|
|
758
895
|
console.error(`[RSC] Action error:`, error);
|
|
759
896
|
throw error;
|
|
760
897
|
}
|
|
761
|
-
}
|
|
762
898
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
899
|
+
// Revalidation render wrapped in route middleware.
|
|
900
|
+
// Actions from client-side navigation include _rsc_partial — preserve
|
|
901
|
+
// the partial flag so the revalidation returns a Flight stream, not HTML.
|
|
902
|
+
// App-switch is already excluded by classifyRequest (would be full-render).
|
|
903
|
+
const isPartialAction = url.searchParams.has("_rsc_partial");
|
|
904
|
+
return executeRenderWithMiddleware(
|
|
905
|
+
plan.route.routeMiddleware,
|
|
906
|
+
plan.negotiated,
|
|
907
|
+
plan.route.routeKey,
|
|
908
|
+
routeReverse,
|
|
767
909
|
request,
|
|
768
910
|
env,
|
|
769
911
|
url,
|
|
770
912
|
variables,
|
|
771
913
|
nonce,
|
|
772
|
-
preview?.params,
|
|
773
|
-
preview?.routeKey,
|
|
774
914
|
handleStore,
|
|
915
|
+
isPartialAction,
|
|
775
916
|
actionContinuation,
|
|
776
917
|
);
|
|
777
|
-
|
|
778
|
-
response.headers.append("Vary", "Accept");
|
|
779
|
-
}
|
|
780
|
-
return response;
|
|
781
|
-
};
|
|
782
|
-
|
|
783
|
-
// Wrap the render path (with or without route middleware) in a
|
|
784
|
-
// renderStartMs timeout so slow renders are caught before output.
|
|
785
|
-
const executeRender = async (): Promise<Response> => {
|
|
786
|
-
if (preview?.routeMiddleware && preview.routeMiddleware.length > 0) {
|
|
787
|
-
const mwResponse = await executeMiddleware(
|
|
788
|
-
buildRouteMiddlewareEntries<TEnv>(preview.routeMiddleware),
|
|
789
|
-
request,
|
|
790
|
-
env,
|
|
791
|
-
variables,
|
|
792
|
-
renderHandler,
|
|
793
|
-
routeReverse,
|
|
794
|
-
);
|
|
795
|
-
|
|
796
|
-
if (
|
|
797
|
-
url.searchParams.has("_rsc_partial") ||
|
|
798
|
-
url.searchParams.has("_rsc_action")
|
|
799
|
-
) {
|
|
800
|
-
const intercepted = interceptRedirectForPartial(
|
|
801
|
-
mwResponse,
|
|
802
|
-
createRedirectFlightResponse,
|
|
803
|
-
);
|
|
804
|
-
if (intercepted) return intercepted;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
return finalizeResponse(mwResponse);
|
|
808
|
-
}
|
|
918
|
+
}
|
|
809
919
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
920
|
+
// ---- Full render / Partial render (or PE that fell through) ----
|
|
921
|
+
if (plan.mode === "full-render" || plan.mode === "partial-render") {
|
|
922
|
+
const isPartial = plan.mode === "partial-render";
|
|
923
|
+
return executeRenderWithMiddleware(
|
|
924
|
+
plan.route.routeMiddleware,
|
|
925
|
+
plan.negotiated,
|
|
926
|
+
plan.route.routeKey,
|
|
927
|
+
routeReverse,
|
|
928
|
+
request,
|
|
929
|
+
env,
|
|
930
|
+
url,
|
|
931
|
+
variables,
|
|
932
|
+
nonce,
|
|
933
|
+
handleStore,
|
|
934
|
+
isPartial,
|
|
935
|
+
);
|
|
936
|
+
}
|
|
813
937
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
938
|
+
// PE that fell through (handleProgressiveEnhancement returned null)
|
|
939
|
+
// falls back to full render
|
|
940
|
+
if (plan.mode === "pe-render") {
|
|
941
|
+
return executeRenderWithMiddleware(
|
|
942
|
+
plan.route.routeMiddleware,
|
|
943
|
+
false,
|
|
944
|
+
plan.route.routeKey,
|
|
945
|
+
routeReverse,
|
|
821
946
|
request,
|
|
822
947
|
env,
|
|
823
948
|
url,
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
949
|
+
variables,
|
|
950
|
+
nonce,
|
|
951
|
+
handleStore,
|
|
952
|
+
false,
|
|
827
953
|
);
|
|
828
954
|
}
|
|
829
|
-
|
|
955
|
+
|
|
956
|
+
// Redirect plan that wasn't handled above (full-page redirect — let
|
|
957
|
+
// the pipeline handle it via match() which returns { redirect: url })
|
|
958
|
+
return executeRenderWithMiddleware(
|
|
959
|
+
plan.route.routeMiddleware,
|
|
960
|
+
false,
|
|
961
|
+
plan.route.routeKey,
|
|
962
|
+
routeReverse,
|
|
963
|
+
request,
|
|
964
|
+
env,
|
|
965
|
+
url,
|
|
966
|
+
variables,
|
|
967
|
+
nonce,
|
|
968
|
+
handleStore,
|
|
969
|
+
false,
|
|
970
|
+
);
|
|
830
971
|
}
|
|
831
972
|
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
async function
|
|
973
|
+
// Shared render execution: wraps handleRscRendering (or revalidateAfterAction)
|
|
974
|
+
// in route middleware and timeout handling. Consolidates the pattern used by
|
|
975
|
+
// action-revalidate, full-render, and partial-render modes.
|
|
976
|
+
async function executeRenderWithMiddleware(
|
|
977
|
+
routeMiddleware: import("../router/middleware-types.js").CollectedMiddleware[],
|
|
978
|
+
negotiated: boolean,
|
|
979
|
+
routeKey: string,
|
|
980
|
+
routeReverse: ReturnType<typeof createReverseFunction>,
|
|
836
981
|
request: Request,
|
|
837
982
|
env: TEnv,
|
|
838
983
|
url: URL,
|
|
839
984
|
variables: Record<string, any>,
|
|
840
985
|
nonce: string | undefined,
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
handleStore?: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
986
|
+
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
987
|
+
isPartial: boolean,
|
|
844
988
|
actionContinuation?: ActionContinuation,
|
|
845
989
|
): Promise<Response> {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
990
|
+
const renderHandler = async (): Promise<Response> => {
|
|
991
|
+
try {
|
|
992
|
+
let response: Response;
|
|
993
|
+
if (actionContinuation) {
|
|
994
|
+
response = await revalidateAfterAction(
|
|
995
|
+
handlerCtx,
|
|
996
|
+
request,
|
|
997
|
+
env,
|
|
998
|
+
url,
|
|
999
|
+
handleStore,
|
|
1000
|
+
actionContinuation,
|
|
1001
|
+
);
|
|
1002
|
+
} else {
|
|
1003
|
+
response = await handleRscRendering(
|
|
1004
|
+
handlerCtx,
|
|
1005
|
+
request,
|
|
1006
|
+
env,
|
|
1007
|
+
url,
|
|
1008
|
+
isPartial,
|
|
1009
|
+
handleStore,
|
|
1010
|
+
nonce,
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
if (negotiated) {
|
|
1014
|
+
response.headers.append("Vary", "Accept");
|
|
1015
|
+
}
|
|
1016
|
+
return response;
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
// Check if middleware/handler returned Response
|
|
1019
|
+
if (error instanceof Response) {
|
|
1020
|
+
// During partial (client-side navigation), a 200 Response from a handler
|
|
1021
|
+
// means the route serves raw content (JSON, text, etc.), not JSX.
|
|
1022
|
+
// Signal the browser to hard-navigate so it renders the raw response.
|
|
1023
|
+
if (isPartial && error.status === 200) {
|
|
1024
|
+
console.warn(
|
|
1025
|
+
`[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
|
|
1026
|
+
`Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
|
|
1027
|
+
);
|
|
1028
|
+
return createResponseWithMergedHeaders(null, {
|
|
1029
|
+
status: 200,
|
|
1030
|
+
headers: {
|
|
1031
|
+
"X-RSC-Reload": stripInternalParams(url).toString(),
|
|
1032
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
1033
|
+
},
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
861
1036
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
if (referer) {
|
|
869
|
-
try {
|
|
870
|
-
const refererUrl = new URL(referer);
|
|
871
|
-
if (refererUrl.origin === url.origin) {
|
|
872
|
-
reloadUrl = referer;
|
|
873
|
-
}
|
|
874
|
-
} catch {
|
|
875
|
-
// Malformed referer, fall back to cleanUrl
|
|
1037
|
+
if (isPartial) {
|
|
1038
|
+
const intercepted = interceptRedirectForPartial(
|
|
1039
|
+
error,
|
|
1040
|
+
createRedirectFlightResponse,
|
|
1041
|
+
);
|
|
1042
|
+
if (intercepted) return intercepted;
|
|
876
1043
|
}
|
|
1044
|
+
|
|
1045
|
+
return error;
|
|
877
1046
|
}
|
|
878
|
-
}
|
|
879
1047
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
892
|
-
if (
|
|
893
|
-
url.searchParams.has("__debug_manifest") &&
|
|
894
|
-
(isDev || router.allowDebugManifest)
|
|
895
|
-
) {
|
|
896
|
-
const trie = getRouterTrie(router.id) ?? getRouteTrie();
|
|
897
|
-
const routeManifest = getRequiredRouteMap();
|
|
898
|
-
const { extractAncestryFromTrie } =
|
|
899
|
-
await import("../build/route-trie.js");
|
|
900
|
-
return new Response(
|
|
901
|
-
JSON.stringify(
|
|
902
|
-
{
|
|
903
|
-
routerId: router.id,
|
|
904
|
-
routeManifest,
|
|
905
|
-
routeAncestry: trie ? extractAncestryFromTrie(trie) : {},
|
|
906
|
-
routeTrie: trie,
|
|
907
|
-
precomputedEntries: getPrecomputedEntries(),
|
|
908
|
-
},
|
|
909
|
-
null,
|
|
910
|
-
2,
|
|
911
|
-
),
|
|
912
|
-
{
|
|
913
|
-
headers: { "Content-Type": "application/json" },
|
|
914
|
-
},
|
|
915
|
-
);
|
|
916
|
-
}
|
|
1048
|
+
// Render 404 page for unmatched routes
|
|
1049
|
+
const isRouteNotFound =
|
|
1050
|
+
error instanceof RouteNotFoundError ||
|
|
1051
|
+
(error instanceof Error && error.name === "RouteNotFoundError");
|
|
1052
|
+
if (isRouteNotFound) {
|
|
1053
|
+
callOnError(error, "routing", {
|
|
1054
|
+
request,
|
|
1055
|
+
url,
|
|
1056
|
+
env,
|
|
1057
|
+
handledByBoundary: true,
|
|
1058
|
+
});
|
|
917
1059
|
|
|
918
|
-
|
|
1060
|
+
const notFoundOption = router.notFound;
|
|
1061
|
+
const notFoundComponent =
|
|
1062
|
+
typeof notFoundOption === "function"
|
|
1063
|
+
? notFoundOption({ pathname: url.pathname })
|
|
1064
|
+
: (notFoundOption ?? createElement("h1", null, "Not Found"));
|
|
1065
|
+
|
|
1066
|
+
const notFoundSegment = {
|
|
1067
|
+
id: "notFound",
|
|
1068
|
+
namespace: "notFound",
|
|
1069
|
+
type: "route" as const,
|
|
1070
|
+
index: 0,
|
|
1071
|
+
component: notFoundComponent,
|
|
1072
|
+
params: {},
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
const payload: RscPayload = {
|
|
1076
|
+
metadata: {
|
|
1077
|
+
pathname: url.pathname,
|
|
1078
|
+
routerId: router.id,
|
|
1079
|
+
basename: router.basename,
|
|
1080
|
+
segments: [notFoundSegment],
|
|
1081
|
+
matched: [],
|
|
1082
|
+
diff: [],
|
|
1083
|
+
isPartial: false,
|
|
1084
|
+
rootLayout: router.rootLayout,
|
|
1085
|
+
handles: handleStore.stream(),
|
|
1086
|
+
version,
|
|
1087
|
+
themeConfig: router.themeConfig,
|
|
1088
|
+
warmupEnabled: router.warmupEnabled,
|
|
1089
|
+
initialTheme: requireRequestContext().theme,
|
|
1090
|
+
},
|
|
1091
|
+
};
|
|
919
1092
|
|
|
920
|
-
|
|
921
|
-
// Route params were already set in coreRequestHandler, but set again
|
|
922
|
-
// for callers that enter coreRequestHandlerInner directly.
|
|
923
|
-
if (routeParams) {
|
|
924
|
-
setRequestContextParams(routeParams, routeKey);
|
|
925
|
-
}
|
|
1093
|
+
const rscStream = renderToReadableStream(payload);
|
|
926
1094
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
handlerCtx,
|
|
933
|
-
request,
|
|
934
|
-
env,
|
|
935
|
-
url,
|
|
936
|
-
store,
|
|
937
|
-
actionContinuation,
|
|
938
|
-
);
|
|
939
|
-
}
|
|
1095
|
+
const isRscRequest =
|
|
1096
|
+
isPartial ||
|
|
1097
|
+
(!request.headers.get("accept")?.includes("text/html") &&
|
|
1098
|
+
!url.searchParams.has("__html")) ||
|
|
1099
|
+
url.searchParams.has("__rsc");
|
|
940
1100
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
handlerCtx,
|
|
948
|
-
request,
|
|
949
|
-
env,
|
|
950
|
-
url,
|
|
951
|
-
variables,
|
|
952
|
-
routeParams,
|
|
953
|
-
);
|
|
954
|
-
}
|
|
1101
|
+
if (isRscRequest) {
|
|
1102
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
1103
|
+
status: 404,
|
|
1104
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
955
1107
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
request,
|
|
963
|
-
env,
|
|
964
|
-
url,
|
|
965
|
-
isPartial,
|
|
966
|
-
store,
|
|
967
|
-
nonce,
|
|
968
|
-
);
|
|
969
|
-
} catch (error) {
|
|
970
|
-
// Check if middleware/handler returned Response
|
|
971
|
-
if (error instanceof Response) {
|
|
972
|
-
// During partial (client-side navigation), a 200 Response from a handler
|
|
973
|
-
// means the route serves raw content (JSON, text, etc.), not JSX.
|
|
974
|
-
// Signal the browser to hard-navigate so it renders the raw response.
|
|
975
|
-
// Only for 200 — redirects (3xx) work already because the browser follows
|
|
976
|
-
// them automatically to a URL that serves Flight data.
|
|
977
|
-
if (isPartial && error.status === 200) {
|
|
978
|
-
console.warn(
|
|
979
|
-
`[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
|
|
980
|
-
`Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
|
|
1108
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
1109
|
+
handlerCtx,
|
|
1110
|
+
request,
|
|
1111
|
+
env,
|
|
1112
|
+
url,
|
|
1113
|
+
requireRequestContext()._metricsStore,
|
|
981
1114
|
);
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
"X-RSC-Reload": stripInternalParams(url).toString(),
|
|
986
|
-
"content-type": "text/x-component;charset=utf-8",
|
|
987
|
-
},
|
|
1115
|
+
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
1116
|
+
nonce,
|
|
1117
|
+
streamMode,
|
|
988
1118
|
});
|
|
989
|
-
}
|
|
990
1119
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
);
|
|
996
|
-
if (intercepted) return intercepted;
|
|
1120
|
+
return createResponseWithMergedHeaders(htmlStream, {
|
|
1121
|
+
status: 404,
|
|
1122
|
+
headers: { "content-type": "text/html;charset=utf-8" },
|
|
1123
|
+
});
|
|
997
1124
|
}
|
|
998
1125
|
|
|
999
|
-
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
// Render 404 page for unmatched routes
|
|
1003
|
-
// Check both instanceof and error.name for cross-bundle compatibility
|
|
1004
|
-
const isRouteNotFound =
|
|
1005
|
-
error instanceof RouteNotFoundError ||
|
|
1006
|
-
(error instanceof Error && error.name === "RouteNotFoundError");
|
|
1007
|
-
if (isRouteNotFound) {
|
|
1126
|
+
// Report unhandled errors
|
|
1008
1127
|
callOnError(error, "routing", {
|
|
1009
1128
|
request,
|
|
1010
1129
|
url,
|
|
1011
1130
|
env,
|
|
1012
|
-
handledByBoundary:
|
|
1131
|
+
handledByBoundary: false,
|
|
1013
1132
|
});
|
|
1133
|
+
console.error(`[RSC] Error:`, error);
|
|
1134
|
+
throw error;
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1014
1137
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
: (notFoundOption ?? createElement("h1", null, "Not Found"));
|
|
1021
|
-
|
|
1022
|
-
// Create a simple segment for the 404 page
|
|
1023
|
-
const notFoundSegment = {
|
|
1024
|
-
id: "notFound",
|
|
1025
|
-
namespace: "notFound",
|
|
1026
|
-
type: "route" as const,
|
|
1027
|
-
index: 0,
|
|
1028
|
-
component: notFoundComponent,
|
|
1029
|
-
params: {},
|
|
1030
|
-
};
|
|
1031
|
-
|
|
1032
|
-
const payload: RscPayload = {
|
|
1033
|
-
metadata: {
|
|
1034
|
-
pathname: url.pathname,
|
|
1035
|
-
routerId: router.id,
|
|
1036
|
-
basename: router.basename,
|
|
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,
|
|
1138
|
+
// Wrap the render path in a renderStartMs timeout
|
|
1139
|
+
const executeRender = async (): Promise<Response> => {
|
|
1140
|
+
if (routeMiddleware.length > 0) {
|
|
1141
|
+
const mwResponse = await executeMiddleware(
|
|
1142
|
+
buildRouteMiddlewareEntries<TEnv>(routeMiddleware),
|
|
1071
1143
|
request,
|
|
1072
1144
|
env,
|
|
1073
|
-
|
|
1074
|
-
|
|
1145
|
+
variables,
|
|
1146
|
+
renderHandler,
|
|
1147
|
+
routeReverse,
|
|
1075
1148
|
);
|
|
1076
|
-
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
1077
|
-
nonce,
|
|
1078
|
-
streamMode,
|
|
1079
|
-
});
|
|
1080
1149
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1150
|
+
if (isPartial || actionContinuation) {
|
|
1151
|
+
const intercepted = interceptRedirectForPartial(
|
|
1152
|
+
mwResponse,
|
|
1153
|
+
createRedirectFlightResponse,
|
|
1154
|
+
);
|
|
1155
|
+
if (intercepted) return intercepted;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
return finalizeResponse(mwResponse);
|
|
1085
1159
|
}
|
|
1086
1160
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1161
|
+
return renderHandler();
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
const renderOutcome = await withTimeout(
|
|
1165
|
+
executeRender(),
|
|
1166
|
+
router.timeouts.renderStartMs,
|
|
1167
|
+
"render-start",
|
|
1168
|
+
);
|
|
1169
|
+
if (renderOutcome.timedOut) {
|
|
1170
|
+
return handleTimeoutResponse(
|
|
1089
1171
|
request,
|
|
1090
|
-
url,
|
|
1091
1172
|
env,
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1173
|
+
url,
|
|
1174
|
+
"render-start",
|
|
1175
|
+
renderOutcome.durationMs,
|
|
1176
|
+
routeKey,
|
|
1177
|
+
);
|
|
1096
1178
|
}
|
|
1179
|
+
return renderOutcome.result;
|
|
1097
1180
|
}
|
|
1098
1181
|
}
|