@rangojs/router 0.27.0 → 0.29.0
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/bin/rango.js +1 -1
- package/dist/types/__internal.d.ts +7 -1
- package/dist/types/browser/action-fence.d.ts +2 -1
- package/dist/types/browser/event-controller.d.ts +8 -2
- package/dist/types/browser/history-state.d.ts +3 -9
- package/dist/types/browser/navigation-store.d.ts +8 -6
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
- package/dist/types/browser/react/use-action.d.ts +7 -2
- package/dist/types/browser/react/use-handle.d.ts +19 -16
- package/dist/types/browser/react/use-link-status.d.ts +5 -2
- package/dist/types/browser/segment-reconciler.d.ts +32 -4
- package/dist/types/browser/types.d.ts +24 -5
- package/dist/types/browser/wire-protocol.d.ts +26 -0
- package/dist/types/cache/cache-key-utils.d.ts +11 -5
- package/dist/types/cache/cache-scope.d.ts +34 -22
- package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
- package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
- package/dist/types/cache/handle-snapshot.d.ts +9 -4
- package/dist/types/cache/segment-codec.d.ts +14 -0
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/taint.d.ts +5 -3
- package/dist/types/cache/types.d.ts +7 -3
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
- package/dist/types/prefetch-markers.d.ts +5 -14
- package/dist/types/prerender/store.d.ts +31 -0
- package/dist/types/prerender/warm-request.d.ts +3 -3
- package/dist/types/prerender.d.ts +2 -2
- package/dist/types/route-name.d.ts +5 -0
- package/dist/types/router/error-handling.d.ts +11 -5
- package/dist/types/router/instrument.d.ts +4 -3
- package/dist/types/router/intercept-resolution.d.ts +25 -9
- package/dist/types/router/match-context.d.ts +6 -11
- package/dist/types/router/match-handlers.d.ts +2 -6
- package/dist/types/router/match-middleware/background-revalidation.d.ts +13 -22
- package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
- package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
- package/dist/types/router/navigation-snapshot.d.ts +7 -0
- package/dist/types/router/prerender-match.d.ts +21 -6
- package/dist/types/router/preview-match.d.ts +2 -7
- package/dist/types/router/request-classification.d.ts +5 -0
- package/dist/types/router/router-context.d.ts +7 -46
- package/dist/types/router/router-interfaces.d.ts +8 -14
- package/dist/types/router/segment-resolution/helpers.d.ts +19 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
- package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
- package/dist/types/router/segment-resolution/resolution-pass.d.ts +95 -0
- package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +2 -0
- package/dist/types/router/segment-resolution.d.ts +1 -2
- package/dist/types/router/segment-wrappers.d.ts +11 -48
- package/dist/types/router/telemetry.d.ts +2 -0
- package/dist/types/router/types.d.ts +11 -1
- package/dist/types/rsc/full-payload.d.ts +2 -2
- package/dist/types/rsc/handler-context.d.ts +6 -6
- package/dist/types/rsc/json-route-result.d.ts +5 -9
- package/dist/types/rsc/request-stages.d.ts +203 -0
- package/dist/types/rsc/response-cache-serve.d.ts +7 -12
- package/dist/types/rsc/response-route-handler.d.ts +2 -2
- package/dist/types/rsc/routine-plan.d.ts +1 -1
- package/dist/types/rsc/server-action.d.ts +2 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
- package/dist/types/rsc/shell-capture.d.ts +132 -15
- package/dist/types/rsc/shell-serve.d.ts +37 -12
- package/dist/types/server/context.d.ts +9 -8
- package/dist/types/server/handle-store.d.ts +16 -0
- package/dist/types/server/render-pass.d.ts +72 -0
- package/dist/types/server/request-context.d.ts +53 -104
- package/dist/types/server/rewrite-trail.d.ts +2 -2
- package/dist/types/testing/dispatch.d.ts +35 -18
- package/dist/types/testing/render-route.d.ts +43 -8
- package/dist/types/testing/serve-shell-request.d.ts +8 -1
- package/dist/types/types/cache-types.d.ts +6 -3
- package/dist/types/types/error-types.d.ts +30 -3
- package/dist/types/types/handler-context.d.ts +9 -1
- package/dist/types/types/segments.d.ts +1 -1
- package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
- package/dist/types/vite/discovery/build-env.d.ts +22 -0
- package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
- package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
- package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
- package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +12 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
- package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +8 -47
- package/dist/types/vite/temp-rsc-server.d.ts +39 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
- package/dist/types/wire-params.d.ts +39 -0
- package/dist/vite/index.js +3822 -3727
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +8 -5
- package/skills/caching/SKILL.md +37 -18
- package/skills/client-urls/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +13 -1
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/hooks/SKILL.md +1 -1
- package/skills/hooks/data.md +8 -1
- package/skills/hooks/handle-and-actions.md +33 -2
- package/skills/hooks/navigation.md +121 -15
- package/skills/hooks/state.md +6 -4
- package/skills/hooks/urls.md +16 -5
- package/skills/intercept/SKILL.md +23 -7
- package/skills/middleware/SKILL.md +18 -0
- package/skills/observability/SKILL.md +30 -0
- package/skills/ppr/SKILL.md +18 -3
- package/skills/prerender/SKILL.md +39 -15
- package/skills/rango/SKILL.md +5 -1
- package/skills/server-actions/SKILL.md +15 -1
- package/skills/testing/client-components.md +25 -18
- package/skills/testing/response-routes.md +6 -4
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/use-cache/SKILL.md +46 -8
- package/skills/view-transitions/SKILL.md +1 -1
- package/src/__internal.ts +7 -1
- package/src/browser/action-fence.ts +2 -1
- package/src/browser/connection-warmup.ts +3 -1
- package/src/browser/event-controller.ts +37 -13
- package/src/browser/history-state.ts +6 -12
- package/src/browser/navigation-bridge.ts +59 -26
- package/src/browser/navigation-client.ts +20 -25
- package/src/browser/navigation-store.ts +25 -27
- package/src/browser/navigation-transaction.ts +4 -1
- package/src/browser/network-error-handler.ts +4 -0
- package/src/browser/partial-update.ts +33 -6
- package/src/browser/prefetch/cache.ts +2 -1
- package/src/browser/prefetch/fetch.ts +8 -16
- package/src/browser/prefetch/queue.ts +4 -4
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/filter-segment-order.ts +2 -2
- package/src/browser/react/use-action.ts +24 -48
- package/src/browser/react/use-handle.ts +22 -18
- package/src/browser/react/use-link-status.ts +48 -60
- package/src/browser/react/use-navigation.ts +92 -41
- package/src/browser/rsc-router.tsx +1 -0
- package/src/browser/segment-reconciler.ts +109 -53
- package/src/browser/server-action-bridge.ts +81 -61
- package/src/browser/types.ts +25 -5
- package/src/browser/wire-protocol.ts +43 -0
- package/src/cache/cache-key-utils.ts +11 -5
- package/src/cache/cache-runtime.ts +132 -75
- package/src/cache/cache-scope.ts +110 -64
- package/src/cache/cf/cf-cache-constants.ts +2 -2
- package/src/cache/cf/cf-cache-store.ts +486 -874
- package/src/cache/cf/cf-kv-utils.ts +1 -1
- package/src/cache/document-cache.ts +64 -46
- package/src/cache/handle-snapshot.ts +19 -7
- package/src/cache/read-through-swr.ts +20 -18
- package/src/cache/segment-codec.ts +55 -0
- package/src/cache/shell-snapshot.ts +2 -2
- package/src/cache/tag-invalidation.ts +6 -2
- package/src/cache/taint.ts +5 -3
- package/src/cache/types.ts +7 -3
- package/src/cache/vercel/vercel-cache-store.ts +243 -310
- package/src/handles/pushed-by.ts +1 -1
- package/src/prefetch-markers.ts +5 -16
- package/src/prerender/store.ts +76 -0
- package/src/prerender/warm-request.ts +8 -4
- package/src/prerender.ts +2 -2
- package/src/route-name.ts +8 -0
- package/src/router/error-handling.ts +22 -10
- package/src/router/instrument.ts +4 -3
- package/src/router/intercept-resolution.ts +82 -75
- package/src/router/loader-resolution.ts +5 -4
- package/src/router/match-api.ts +5 -5
- package/src/router/match-context.ts +23 -12
- package/src/router/match-handlers.ts +8 -20
- package/src/router/match-middleware/background-revalidation.ts +46 -57
- package/src/router/match-middleware/cache-lookup.ts +677 -726
- package/src/router/match-middleware/cache-store.ts +4 -8
- package/src/router/match-middleware/intercept-resolution.ts +3 -23
- package/src/router/match-middleware/segment-resolution.ts +34 -61
- package/src/router/middleware.ts +69 -11
- package/src/router/navigation-snapshot.ts +14 -4
- package/src/router/prerender-match.ts +100 -44
- package/src/router/preview-match.ts +6 -41
- package/src/router/request-classification.ts +13 -7
- package/src/router/route-snapshot.ts +2 -5
- package/src/router/router-context.ts +10 -95
- package/src/router/router-interfaces.ts +8 -14
- package/src/router/segment-resolution/helpers.ts +49 -13
- package/src/router/segment-resolution/loader-cache.ts +15 -9
- package/src/router/segment-resolution/loader-mask.ts +5 -5
- package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
- package/src/router/segment-resolution/resolution-pass.ts +139 -0
- package/src/router/segment-resolution/revalidation.ts +939 -789
- package/src/router/segment-resolution/streamed-handler-boundary.ts +3 -0
- package/src/router/segment-resolution.ts +4 -13
- package/src/router/segment-wrappers.ts +51 -269
- package/src/router/telemetry.ts +5 -0
- package/src/router/types.ts +11 -1
- package/src/router.ts +19 -17
- package/src/rsc/full-payload.ts +6 -5
- package/src/rsc/handler-context.ts +8 -14
- package/src/rsc/handler.ts +126 -507
- package/src/rsc/json-route-result.ts +5 -9
- package/src/rsc/loader-fetch.ts +6 -5
- package/src/rsc/progressive-enhancement.ts +5 -0
- package/src/rsc/request-stages.ts +759 -0
- package/src/rsc/response-cache-serve.ts +10 -14
- package/src/rsc/response-route-handler.ts +13 -5
- package/src/rsc/rewrite-passes.ts +2 -1
- package/src/rsc/routine-plan.ts +1 -1
- package/src/rsc/rsc-rendering.ts +164 -349
- package/src/rsc/server-action.ts +87 -113
- package/src/rsc/shell-capture-constants.ts +1 -1
- package/src/rsc/shell-capture.ts +275 -227
- package/src/rsc/shell-serve.ts +121 -18
- package/src/rsc/ssr-setup.ts +5 -4
- package/src/segment-system.tsx +3 -3
- package/src/server/context.ts +22 -25
- package/src/server/handle-store.ts +55 -0
- package/src/server/render-pass.ts +291 -0
- package/src/server/request-context.ts +75 -115
- package/src/server/rewrite-trail.ts +6 -4
- package/src/ssr/index.tsx +1 -1
- package/src/testing/dispatch.ts +226 -710
- package/src/testing/e2e/page-helpers.ts +2 -2
- package/src/testing/render-route.tsx +199 -13
- package/src/testing/serve-shell-request.ts +36 -40
- package/src/types/cache-types.ts +6 -3
- package/src/types/error-types.ts +30 -3
- package/src/types/handler-context.ts +12 -1
- package/src/types/segments.ts +1 -1
- package/src/vite/discovery/build-discovery-phase.ts +204 -0
- package/src/vite/discovery/build-env.ts +148 -0
- package/src/vite/discovery/bundle-scan-phase.ts +128 -0
- package/src/vite/discovery/dev-prerender-cache.ts +8 -3
- package/src/vite/discovery/dev-server-phase.ts +1776 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +1 -1
- package/src/vite/discovery/prerender-collection.ts +24 -39
- package/src/vite/discovery/state.ts +15 -0
- package/src/vite/plugins/version-plugin.ts +1 -1
- package/src/vite/plugins/virtual-entries.ts +1 -1
- package/src/vite/router-discovery.ts +29 -2428
- package/src/vite/temp-rsc-server.ts +223 -0
- package/src/vite/utils/prerender-utils.ts +47 -0
- package/src/wire-params.ts +40 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
- package/src/router/segment-resolution/fresh.ts +0 -945
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request stages
|
|
3
|
+
*
|
|
4
|
+
* The stages a request runs through before and after rendering: classify,
|
|
5
|
+
* guard, the global middleware passes, the response route, and the
|
|
6
|
+
* response's finalization. The RSC handler (rsc/handler.ts createRSCHandler)
|
|
7
|
+
* runs them all around its Flight executor. The dispatch testing primitive
|
|
8
|
+
* (testing/dispatch.ts) runs classify, the terminal plans, the passes, the
|
|
9
|
+
* response route and the finalization, and throws where a plan would render
|
|
10
|
+
* a component. Sharing them keeps dispatch's responses identical to
|
|
11
|
+
* production's; a hand-kept mirror drifted once (#1128).
|
|
12
|
+
*
|
|
13
|
+
* No import of @vitejs/plugin-rsc on this graph: dispatch loads it in a
|
|
14
|
+
* plain Vitest project, where plugin-rsc's entries cannot load (they import
|
|
15
|
+
* Vite virtuals). testing/__tests__/testing-module-graph.test.ts pins it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { requestHeaders } from "../server/request-headers.js";
|
|
19
|
+
import { isRouteNotFoundError } from "../errors.js";
|
|
20
|
+
import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
|
|
21
|
+
import type { MiddlewareEntry } from "../router/middleware-types.js";
|
|
22
|
+
import {
|
|
23
|
+
_getRequestContext,
|
|
24
|
+
getRequestContext,
|
|
25
|
+
reportedErrorsOf,
|
|
26
|
+
setRequestContextParams,
|
|
27
|
+
type RequestContext,
|
|
28
|
+
} from "../server/request-context.js";
|
|
29
|
+
import {
|
|
30
|
+
REWRITE_HEADER,
|
|
31
|
+
runRewritePasses,
|
|
32
|
+
type RewritePassesOptions,
|
|
33
|
+
type RewritePassesResult,
|
|
34
|
+
} from "./rewrite-passes.js";
|
|
35
|
+
import { rewriteTrail } from "../server/rewrite-trail.js";
|
|
36
|
+
import {
|
|
37
|
+
createReloadResponse,
|
|
38
|
+
finalizeResponse,
|
|
39
|
+
interceptRedirectForPartial,
|
|
40
|
+
} from "./helpers.js";
|
|
41
|
+
import { guardOutgoingRedirect } from "./redirect-guard.js";
|
|
42
|
+
import {
|
|
43
|
+
isWebSocketUpgradeResponse,
|
|
44
|
+
appendVaryAccept,
|
|
45
|
+
trySetHeader,
|
|
46
|
+
} from "../response-utils.js";
|
|
47
|
+
import {
|
|
48
|
+
handleResponseRoute,
|
|
49
|
+
type ResponseRouteMatch,
|
|
50
|
+
} from "./response-route-handler.js";
|
|
51
|
+
import type { ErrorPhase } from "../types.js";
|
|
52
|
+
import type { OnErrorCallback } from "../types/error-types.js";
|
|
53
|
+
import {
|
|
54
|
+
invokeOnError,
|
|
55
|
+
type InvokeOnErrorContext,
|
|
56
|
+
} from "../router/error-handling.js";
|
|
57
|
+
import { createReverseFunction } from "../router/handler-context.js";
|
|
58
|
+
import type { OriginCheckPhase } from "./origin-guard.js";
|
|
59
|
+
import { applyStreamIdleTimeout } from "./stream-idle.js";
|
|
60
|
+
import {
|
|
61
|
+
withTimeout,
|
|
62
|
+
isTimeoutEnabled,
|
|
63
|
+
RouterTimeoutError,
|
|
64
|
+
createDefaultTimeoutResponse,
|
|
65
|
+
type OnTimeoutCallback,
|
|
66
|
+
type RenderTimeoutContext,
|
|
67
|
+
type ResolvedTimeouts,
|
|
68
|
+
type TimeoutPhase,
|
|
69
|
+
} from "../router/timeout.js";
|
|
70
|
+
import {
|
|
71
|
+
safeEmit,
|
|
72
|
+
resolveSink,
|
|
73
|
+
getRequestId,
|
|
74
|
+
setRequestId,
|
|
75
|
+
type TelemetrySink,
|
|
76
|
+
} from "../router/telemetry.js";
|
|
77
|
+
import {
|
|
78
|
+
classifyRequest,
|
|
79
|
+
type ClassifyRequestDeps,
|
|
80
|
+
type ExecutableRequestPlan,
|
|
81
|
+
type RequestPlan,
|
|
82
|
+
type ResponseRoutePlan,
|
|
83
|
+
} from "../router/request-classification.js";
|
|
84
|
+
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
85
|
+
import type { CacheErrorCategory } from "../cache/cache-error.js";
|
|
86
|
+
import { WIRE } from "../wire-params.js";
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The router members the stages read. The handler's `RangoInternal` carries
|
|
90
|
+
* them; dispatch reads them off the router a test passes.
|
|
91
|
+
*/
|
|
92
|
+
export interface StageRouter<TEnv = unknown> {
|
|
93
|
+
readonly basename: string | undefined;
|
|
94
|
+
readonly middleware: MiddlewareEntry<TEnv>[];
|
|
95
|
+
readonly onError?: OnErrorCallback<TEnv>;
|
|
96
|
+
readonly telemetry?: TelemetrySink;
|
|
97
|
+
readonly timeouts?: ResolvedTimeouts;
|
|
98
|
+
readonly onTimeout?: OnTimeoutCallback<TEnv>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Report an error through the router's onError. */
|
|
102
|
+
export type ReportError<TEnv = unknown> = (
|
|
103
|
+
error: unknown,
|
|
104
|
+
phase: ErrorPhase,
|
|
105
|
+
context: InvokeOnErrorContext<TEnv>,
|
|
106
|
+
) => void;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* What the stages call back into. The handler's context bundle
|
|
110
|
+
* (rsc/handler-context.ts HandlerContext) extends it.
|
|
111
|
+
*/
|
|
112
|
+
export interface RequestStageContext<TEnv = unknown> {
|
|
113
|
+
router: StageRouter<TEnv>;
|
|
114
|
+
callOnError: ReportError<TEnv>;
|
|
115
|
+
getRequiredRouteMap: () => Record<string, string>;
|
|
116
|
+
/**
|
|
117
|
+
* The redirect a partial or action request receives: a Flight payload in
|
|
118
|
+
* the handler (fetch() would follow a raw 3xx to HTML). dispatch has no
|
|
119
|
+
* Flight runtime and answers 204 + X-RSC-Redirect.
|
|
120
|
+
*/
|
|
121
|
+
createRedirectFlightResponse: (
|
|
122
|
+
redirectUrl: string,
|
|
123
|
+
locationState?: Record<string, unknown>,
|
|
124
|
+
external?: boolean,
|
|
125
|
+
) => Response;
|
|
126
|
+
/**
|
|
127
|
+
* Reports a response-route handler's own throw; absent, it goes to
|
|
128
|
+
* callOnError with phase "handler". dispatch passes a no-op: it does not
|
|
129
|
+
* report that throw (its documented boundary, testing/dispatch.ts).
|
|
130
|
+
*/
|
|
131
|
+
reportResponseRouteError?: (
|
|
132
|
+
error: unknown,
|
|
133
|
+
context: InvokeOnErrorContext<TEnv>,
|
|
134
|
+
) => void;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** What classify and the terminal plans read beyond the stage context. */
|
|
138
|
+
export interface ClassifyContext<TEnv = unknown> {
|
|
139
|
+
router: {
|
|
140
|
+
readonly id: string;
|
|
141
|
+
readonly findMatch: ClassifyRequestDeps<TEnv>["findMatch"];
|
|
142
|
+
};
|
|
143
|
+
/** The router's DOCUMENT version: the `_rsc_v` reload check. */
|
|
144
|
+
version: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Per-request error reporter that deduplicates via the ALS request context.
|
|
149
|
+
*
|
|
150
|
+
* Uses the same _reportedErrors WeakSet as the router layer so errors
|
|
151
|
+
* that propagate across layers are only reported once per request.
|
|
152
|
+
*/
|
|
153
|
+
export function createErrorReporter<TEnv>(router: {
|
|
154
|
+
readonly onError?: OnErrorCallback<TEnv>;
|
|
155
|
+
}): ReportError<TEnv> {
|
|
156
|
+
return (error, phase, context) => {
|
|
157
|
+
// Guard: abort signal handlers fire asynchronously outside the ALS
|
|
158
|
+
// request scope, so the context may be gone. Skip dedup in that
|
|
159
|
+
// case — the error is from a cancelled stream, not a real failure.
|
|
160
|
+
const reqCtx = _getRequestContext();
|
|
161
|
+
if (error != null && typeof error === "object" && reqCtx) {
|
|
162
|
+
const reportedErrors = reportedErrorsOf(reqCtx);
|
|
163
|
+
if (reportedErrors.has(error)) return;
|
|
164
|
+
reportedErrors.add(error);
|
|
165
|
+
}
|
|
166
|
+
invokeOnError(router.onError, error, phase, context, "RSC");
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Wire background error reporting so "use cache" and other subsystems can
|
|
172
|
+
* surface non-fatal errors through the router's onError callback.
|
|
173
|
+
*/
|
|
174
|
+
export function reportBackgroundErrors<TEnv>(
|
|
175
|
+
reqCtx: RequestContext<TEnv>,
|
|
176
|
+
request: Request,
|
|
177
|
+
url: URL,
|
|
178
|
+
callOnError: ReportError<TEnv>,
|
|
179
|
+
): void {
|
|
180
|
+
reqCtx._reportBackgroundError = (
|
|
181
|
+
error: unknown,
|
|
182
|
+
category: CacheErrorCategory,
|
|
183
|
+
) => {
|
|
184
|
+
callOnError(error, "cache", {
|
|
185
|
+
request,
|
|
186
|
+
url,
|
|
187
|
+
metadata: { category },
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Classify a request into its plan. classifyRequest throws
|
|
194
|
+
* RouteNotFoundError for an unknown route; that falls through to a
|
|
195
|
+
* full-render plan with no matched route, so the render path renders the
|
|
196
|
+
* 404 page (match()/matchPartial() re-throw and the render's catch renders
|
|
197
|
+
* it).
|
|
198
|
+
*/
|
|
199
|
+
export async function classify<TEnv>(
|
|
200
|
+
request: Request,
|
|
201
|
+
url: URL,
|
|
202
|
+
ctx: ClassifyContext<TEnv>,
|
|
203
|
+
): Promise<RequestPlan<TEnv>> {
|
|
204
|
+
try {
|
|
205
|
+
return await classifyRequest<TEnv>(request, url, {
|
|
206
|
+
findMatch: ctx.router.findMatch,
|
|
207
|
+
routerVersion: ctx.version,
|
|
208
|
+
routerId: ctx.router.id,
|
|
209
|
+
});
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (!isRouteNotFoundError(error)) throw error;
|
|
212
|
+
return {
|
|
213
|
+
mode: "full-render",
|
|
214
|
+
route: {
|
|
215
|
+
matched: null as any,
|
|
216
|
+
manifestEntry: null as any,
|
|
217
|
+
entries: [],
|
|
218
|
+
routeKey: "",
|
|
219
|
+
localRouteName: "",
|
|
220
|
+
params: {},
|
|
221
|
+
routeMiddleware: [],
|
|
222
|
+
cacheScope: null,
|
|
223
|
+
isPassthrough: false,
|
|
224
|
+
},
|
|
225
|
+
negotiated: false,
|
|
226
|
+
unmatched: true,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The plan classify answers an unknown route with (the render path's 404). */
|
|
232
|
+
export function isUnmatchedPlan(plan: RequestPlan<any>): boolean {
|
|
233
|
+
return plan.mode === "full-render" && plan.unmatched === true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Record a plan's route on the request context: its params and name, and the
|
|
238
|
+
* classified snapshot match()/matchPartial() reuse instead of resolving the
|
|
239
|
+
* route again. Runs inside the global chain, so global middleware sees the
|
|
240
|
+
* route after next(). A redirect has none.
|
|
241
|
+
*/
|
|
242
|
+
export function recordPlanRoute<TEnv>(plan: ExecutableRequestPlan<TEnv>): void {
|
|
243
|
+
if (plan.mode === "redirect") return;
|
|
244
|
+
setRequestContextParams(plan.route.params, plan.route.routeKey);
|
|
245
|
+
getRequestContext()._classifiedRoute = plan.route;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The checks between classification and execution that need no await.
|
|
250
|
+
* Returns the response of a terminal plan (a partial request's redirect, a
|
|
251
|
+
* version or app-switch reload), null to go on. Throws for a rewritten
|
|
252
|
+
* action whose target is not a page route.
|
|
253
|
+
*
|
|
254
|
+
* Synchronous, and the origin guard is a separate step: the caller awaits
|
|
255
|
+
* checkRequestOrigin only for the modes ORIGIN_CHECK_PHASE_BY_MODE gates, so
|
|
256
|
+
* a full, partial or response plan reaches execution without an added
|
|
257
|
+
* microtask (rsc/__tests__/request-microtasks.rsc-test.tsx).
|
|
258
|
+
*/
|
|
259
|
+
export function terminalPlanResponse<TEnv>(
|
|
260
|
+
plan: RequestPlan<TEnv>,
|
|
261
|
+
url: URL,
|
|
262
|
+
ctx: RequestStageContext<TEnv> & ClassifyContext<TEnv>,
|
|
263
|
+
): Response | null {
|
|
264
|
+
// A rewritten pass of an action must reach the branch that runs it: the
|
|
265
|
+
// one that ran it re-renders from its outcome, and a JS action a global
|
|
266
|
+
// rewrite moved before it ran runs only on the action branch. Any other
|
|
267
|
+
// plan would serve the POST as a fresh request or drop the action; a
|
|
268
|
+
// reload (version mismatch, app switch) does neither.
|
|
269
|
+
const passCtx = getRequestContext();
|
|
270
|
+
const actionRan = passCtx._actionRan;
|
|
271
|
+
const actionMode = actionRan
|
|
272
|
+
? actionRan.kind === "action"
|
|
273
|
+
? "action"
|
|
274
|
+
: "pe-render"
|
|
275
|
+
: url.searchParams.has(WIRE.action) && rewriteTrail(passCtx)
|
|
276
|
+
? "action"
|
|
277
|
+
: undefined;
|
|
278
|
+
if (
|
|
279
|
+
actionMode &&
|
|
280
|
+
plan.mode !== actionMode &&
|
|
281
|
+
plan.mode !== "version-mismatch" &&
|
|
282
|
+
plan.mode !== "app-switch"
|
|
283
|
+
) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`A rewrite of a server action or form post must target a page ` +
|
|
286
|
+
`route; ${url.pathname} ${plan.mode === "response" ? "is a response route" : plan.mode === "redirect" ? "redirects" : "is not one"}.`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (plan.mode === "redirect") {
|
|
291
|
+
const redirected = partialRedirectResponse(plan.redirectUrl, url, ctx);
|
|
292
|
+
if (redirected) return redirected;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// A reload points at the visitor's URL, never a rewrite target's.
|
|
296
|
+
if (plan.mode === "version-mismatch") {
|
|
297
|
+
console.log(
|
|
298
|
+
`[RSC] Version mismatch: client=${url.searchParams.get(WIRE.version)}, server=${ctx.version}. Forcing reload.`,
|
|
299
|
+
);
|
|
300
|
+
return createReloadResponse(getRequestContext(), plan.reloadUrl);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (plan.mode === "app-switch") {
|
|
304
|
+
// Cross-app SPA navigation crossed a host-router app boundary. Force a
|
|
305
|
+
// real document navigation so the target app's document is re-established
|
|
306
|
+
// (stylesheets, theme, warmup, prefetch-TTL). See request-classification.
|
|
307
|
+
return createReloadResponse(getRequestContext(), plan.reloadUrl);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* The origin guard's rejection: reports it through onError and telemetry,
|
|
315
|
+
* and returns the response checkRequestOrigin rejected the request with. The
|
|
316
|
+
* caller awaits checkRequestOrigin itself, for the modes
|
|
317
|
+
* ORIGIN_CHECK_PHASE_BY_MODE gates (action, loader, PE form), so the guard
|
|
318
|
+
* adds no microtask over that await.
|
|
319
|
+
*/
|
|
320
|
+
export function originRejectedResponse<TEnv>(
|
|
321
|
+
rejection: Response,
|
|
322
|
+
phase: OriginCheckPhase,
|
|
323
|
+
request: Request,
|
|
324
|
+
env: TEnv,
|
|
325
|
+
url: URL,
|
|
326
|
+
ctx: RequestStageContext<TEnv>,
|
|
327
|
+
): Response {
|
|
328
|
+
const origin = requestHeaders(request).get("origin");
|
|
329
|
+
const host = requestHeaders(request).get("host");
|
|
330
|
+
const originError = new Error(
|
|
331
|
+
`Origin check rejected: ${origin ?? "none"} vs ${host ?? "none"}`,
|
|
332
|
+
);
|
|
333
|
+
originError.name = "OriginCheckError";
|
|
334
|
+
|
|
335
|
+
ctx.callOnError(originError, "origin", {
|
|
336
|
+
request,
|
|
337
|
+
url,
|
|
338
|
+
env,
|
|
339
|
+
handledByBoundary: false,
|
|
340
|
+
metadata: { phase, origin, host },
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (ctx.router.telemetry) {
|
|
344
|
+
safeEmit(resolveSink(ctx.router.telemetry), {
|
|
345
|
+
type: "request.origin-rejected",
|
|
346
|
+
timestamp: performance.now(),
|
|
347
|
+
requestId: getRequestId(request),
|
|
348
|
+
method: request.method,
|
|
349
|
+
pathname: url.pathname,
|
|
350
|
+
phase,
|
|
351
|
+
origin,
|
|
352
|
+
host,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return rejection;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* A findMatch() redirect (trailing slash) on a partial request: redirects are
|
|
361
|
+
* handled by the pipeline (match/matchPartial), but a partial request
|
|
362
|
+
* short-circuits with a Flight redirect, since fetch() would follow a raw
|
|
363
|
+
* 3xx. Null on a full request, which the render path redirects (match()
|
|
364
|
+
* returns { redirect: url }).
|
|
365
|
+
*/
|
|
366
|
+
function partialRedirectResponse<TEnv>(
|
|
367
|
+
redirectUrl: string,
|
|
368
|
+
url: URL,
|
|
369
|
+
ctx: RequestStageContext<TEnv>,
|
|
370
|
+
): Response | null {
|
|
371
|
+
return url.searchParams.has(WIRE.partial)
|
|
372
|
+
? ctx.createRedirectFlightResponse(redirectUrl)
|
|
373
|
+
: null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export interface RequestPassesOptions<TEnv> {
|
|
377
|
+
ctx: RequestStageContext<TEnv>;
|
|
378
|
+
reqCtx: RequestContext<TEnv>;
|
|
379
|
+
/** The visitor's request and raw URL: the first pass. */
|
|
380
|
+
request: Request;
|
|
381
|
+
url: URL;
|
|
382
|
+
env: TEnv;
|
|
383
|
+
variables: Record<string, any>;
|
|
384
|
+
/** The visitor's global middleware, when the caller matched it already. */
|
|
385
|
+
firstPassMiddleware?: Array<{
|
|
386
|
+
entry: MiddlewareEntry<TEnv>;
|
|
387
|
+
params: Record<string, string>;
|
|
388
|
+
}>;
|
|
389
|
+
/** Runs before a pass's global chain; dispatch matches the pass there. */
|
|
390
|
+
beforePass?: (request: Request, url: URL, index: number) => Promise<void>;
|
|
391
|
+
/** The core handler a pass's global chain wraps. */
|
|
392
|
+
core: (request: Request, url: URL) => Promise<Response>;
|
|
393
|
+
onHop?: RewritePassesOptions["onHop"];
|
|
394
|
+
refuse?: RewritePassesOptions["refuse"];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export interface RequestPassesResult extends RewritePassesResult {
|
|
398
|
+
/** Whether the last pass ran a global middleware chain. */
|
|
399
|
+
hasMiddleware: boolean;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* One pass per URL: the global middleware chain matched on the pass's path,
|
|
404
|
+
* wrapping the core handler. A `throw rewrite()` from global or route
|
|
405
|
+
* middleware unwinds the pass, and the next one runs the same request as
|
|
406
|
+
* the target from the top, as a direct visit would (rsc/rewrite-passes.ts).
|
|
407
|
+
*/
|
|
408
|
+
export async function runRequestPasses<TEnv>(
|
|
409
|
+
opts: RequestPassesOptions<TEnv>,
|
|
410
|
+
): Promise<RequestPassesResult> {
|
|
411
|
+
const { ctx, env, variables } = opts;
|
|
412
|
+
let passIndex = 0;
|
|
413
|
+
let hasMiddleware = false;
|
|
414
|
+
const runChain = (
|
|
415
|
+
passRequest: Request,
|
|
416
|
+
passUrl: URL,
|
|
417
|
+
index: number,
|
|
418
|
+
): Promise<Response> => {
|
|
419
|
+
const passMiddleware =
|
|
420
|
+
index === 0 && opts.firstPassMiddleware
|
|
421
|
+
? opts.firstPassMiddleware
|
|
422
|
+
: matchMiddleware(passUrl.pathname, ctx.router.middleware);
|
|
423
|
+
hasMiddleware = passMiddleware.length > 0;
|
|
424
|
+
const coreHandler = (): Promise<Response> =>
|
|
425
|
+
opts.core(passRequest, passUrl);
|
|
426
|
+
return hasMiddleware
|
|
427
|
+
? executeMiddleware(
|
|
428
|
+
passMiddleware,
|
|
429
|
+
passRequest,
|
|
430
|
+
env,
|
|
431
|
+
variables,
|
|
432
|
+
coreHandler,
|
|
433
|
+
createReverseFunction(ctx.getRequiredRouteMap()),
|
|
434
|
+
)
|
|
435
|
+
: coreHandler();
|
|
436
|
+
};
|
|
437
|
+
const { response, rewrittenTo } = await runRewritePasses({
|
|
438
|
+
reqCtx: opts.reqCtx,
|
|
439
|
+
request: opts.request,
|
|
440
|
+
url: opts.url,
|
|
441
|
+
routeMap: ctx.getRequiredRouteMap,
|
|
442
|
+
basename: ctx.router.basename,
|
|
443
|
+
runPass: (passRequest, passUrl) => {
|
|
444
|
+
const index = passIndex++;
|
|
445
|
+
return opts.beforePass
|
|
446
|
+
? opts
|
|
447
|
+
.beforePass(passRequest, passUrl, index)
|
|
448
|
+
.then(() => runChain(passRequest, passUrl, index))
|
|
449
|
+
: runChain(passRequest, passUrl, index);
|
|
450
|
+
},
|
|
451
|
+
onHop: (pass) => {
|
|
452
|
+
// A pass's request is a new Request: keep the visitor's telemetry id.
|
|
453
|
+
if (ctx.router.telemetry) {
|
|
454
|
+
setRequestId(pass.request, getRequestId(opts.request));
|
|
455
|
+
}
|
|
456
|
+
opts.onHop?.(pass);
|
|
457
|
+
},
|
|
458
|
+
report: (error, at) => {
|
|
459
|
+
ctx.callOnError(error, "routing", {
|
|
460
|
+
request: at.request,
|
|
461
|
+
url: at.url,
|
|
462
|
+
env,
|
|
463
|
+
handledByBoundary: false,
|
|
464
|
+
});
|
|
465
|
+
},
|
|
466
|
+
refuse: opts.refuse,
|
|
467
|
+
});
|
|
468
|
+
return { response, rewrittenTo, hasMiddleware };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* The flat match handleResponseRoute takes, with params at the top level,
|
|
473
|
+
* built from a response-route plan.
|
|
474
|
+
*/
|
|
475
|
+
export function responseRouteMatch(
|
|
476
|
+
plan: ResponseRoutePlan<any>,
|
|
477
|
+
): ResponseRouteMatch {
|
|
478
|
+
return {
|
|
479
|
+
responseType: plan.responseType,
|
|
480
|
+
handler: plan.handler,
|
|
481
|
+
params: plan.route.params,
|
|
482
|
+
negotiated: plan.negotiated,
|
|
483
|
+
manifestEntry: plan.manifestEntry,
|
|
484
|
+
routeMiddleware: plan.routeMiddleware,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export interface ServeResponseRouteOptions<TEnv> {
|
|
489
|
+
match: ResponseRouteMatch;
|
|
490
|
+
/** The matched route's key, for the timeout report. */
|
|
491
|
+
routeKey: string | undefined;
|
|
492
|
+
/** The pass's request, raw URL and env. */
|
|
493
|
+
request: Request;
|
|
494
|
+
url: URL;
|
|
495
|
+
env: TEnv;
|
|
496
|
+
variables: Record<string, any>;
|
|
497
|
+
/**
|
|
498
|
+
* Receives the handler's work when the deadline wins. The handler leaves
|
|
499
|
+
* it running; dispatch cancels its body.
|
|
500
|
+
*/
|
|
501
|
+
abandon?: (work: Promise<Response>) => void;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* A response route: its handler (rsc/response-route-handler.ts) under the
|
|
506
|
+
* renderStartMs deadline, which starts here, inside the global chain, so
|
|
507
|
+
* global middleware time never counts toward it.
|
|
508
|
+
*/
|
|
509
|
+
export async function serveResponseRoute<TEnv>(
|
|
510
|
+
ctx: RequestStageContext<TEnv>,
|
|
511
|
+
opts: ServeResponseRouteOptions<TEnv>,
|
|
512
|
+
): Promise<Response> {
|
|
513
|
+
const { match, request, env, url } = opts;
|
|
514
|
+
const work = handleResponseRoute(
|
|
515
|
+
ctx,
|
|
516
|
+
match,
|
|
517
|
+
request,
|
|
518
|
+
env,
|
|
519
|
+
url,
|
|
520
|
+
opts.variables,
|
|
521
|
+
);
|
|
522
|
+
const responseOutcome = await withTimeout(
|
|
523
|
+
work,
|
|
524
|
+
ctx.router.timeouts?.renderStartMs,
|
|
525
|
+
"render-start",
|
|
526
|
+
);
|
|
527
|
+
if (responseOutcome.timedOut) {
|
|
528
|
+
opts.abandon?.(work);
|
|
529
|
+
return timeoutResponse(
|
|
530
|
+
ctx,
|
|
531
|
+
request,
|
|
532
|
+
env,
|
|
533
|
+
url,
|
|
534
|
+
"render-start",
|
|
535
|
+
responseOutcome.durationMs,
|
|
536
|
+
opts.routeKey,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const response = responseOutcome.result;
|
|
540
|
+
if (match.negotiated && !isWebSocketUpgradeResponse(response)) {
|
|
541
|
+
// handleResponseRoute (callHandlerWithVary) already appends Vary: Accept
|
|
542
|
+
// for negotiated responses; dedup so we don't emit Vary: Accept, Accept.
|
|
543
|
+
appendVaryAccept(response);
|
|
544
|
+
}
|
|
545
|
+
return response;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Handle a timeout by reporting the error, emitting telemetry,
|
|
550
|
+
* and returning either the custom onTimeout response or a default 504.
|
|
551
|
+
*/
|
|
552
|
+
export async function timeoutResponse<TEnv>(
|
|
553
|
+
ctx: RequestStageContext<TEnv>,
|
|
554
|
+
request: Request,
|
|
555
|
+
env: TEnv,
|
|
556
|
+
url: URL,
|
|
557
|
+
phase: TimeoutPhase,
|
|
558
|
+
durationMs: number,
|
|
559
|
+
routeKey?: string,
|
|
560
|
+
actionId?: string,
|
|
561
|
+
): Promise<Response> {
|
|
562
|
+
const router = ctx.router;
|
|
563
|
+
const timeoutError = new RouterTimeoutError(phase, durationMs);
|
|
564
|
+
const requestContext = _getRequestContext<TEnv>();
|
|
565
|
+
const cursor = requestContext?._renderForeground;
|
|
566
|
+
const render: RenderTimeoutContext | undefined =
|
|
567
|
+
phase === "render-start" && cursor
|
|
568
|
+
? {
|
|
569
|
+
mode: cursor.mode,
|
|
570
|
+
phase: cursor.phase,
|
|
571
|
+
state: cursor.state,
|
|
572
|
+
completed: cursor.completed,
|
|
573
|
+
total: cursor.total,
|
|
574
|
+
...(cursor.phaseStartedAt !== undefined && {
|
|
575
|
+
phaseDurationMs: performance.now() - cursor.phaseStartedAt,
|
|
576
|
+
}),
|
|
577
|
+
}
|
|
578
|
+
: undefined;
|
|
579
|
+
const trace =
|
|
580
|
+
phase === "render-start" ? requestContext?._activeRoutine : undefined;
|
|
581
|
+
const activeEntries = trace?.active() ?? [];
|
|
582
|
+
const activeStep = activeEntries.at(-1);
|
|
583
|
+
const activeAt = performance.now();
|
|
584
|
+
const routineSnapshot =
|
|
585
|
+
trace && activeStep
|
|
586
|
+
? {
|
|
587
|
+
name: trace.name,
|
|
588
|
+
path: activeEntries.map((entry) => entry.name),
|
|
589
|
+
durationMs: activeAt - activeStep.startedAt,
|
|
590
|
+
}
|
|
591
|
+
: undefined;
|
|
592
|
+
|
|
593
|
+
if (INTERNAL_RANGO_DEBUG && trace && activeStep) {
|
|
594
|
+
console.log(
|
|
595
|
+
`[routine] TIMEOUT ${request.method} ${url.pathname} (${trace.name})\n${trace.formatActive(activeEntries, activeAt)}`,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Each surface gets its OWN shallow render snapshot. A consumer that mutates
|
|
600
|
+
// its copy must not corrupt what the other two observe. The internal-debug
|
|
601
|
+
// routine snapshot is bounded metadata sent only to onError.
|
|
602
|
+
ctx.callOnError(timeoutError, phase === "action" ? "action" : "handler", {
|
|
603
|
+
request,
|
|
604
|
+
url,
|
|
605
|
+
env,
|
|
606
|
+
routeKey,
|
|
607
|
+
actionId,
|
|
608
|
+
handledByBoundary: false,
|
|
609
|
+
metadata: {
|
|
610
|
+
timeout: true,
|
|
611
|
+
phase,
|
|
612
|
+
durationMs,
|
|
613
|
+
...(render && { render: { ...render } }),
|
|
614
|
+
...(routineSnapshot && { routine: routineSnapshot }),
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
if (router.telemetry) {
|
|
619
|
+
safeEmit(resolveSink(router.telemetry), {
|
|
620
|
+
type: "request.timeout",
|
|
621
|
+
timestamp: performance.now(),
|
|
622
|
+
requestId: getRequestId(request),
|
|
623
|
+
phase,
|
|
624
|
+
pathname: url.pathname,
|
|
625
|
+
routeKey,
|
|
626
|
+
actionId,
|
|
627
|
+
durationMs,
|
|
628
|
+
customHandler: !!router.onTimeout,
|
|
629
|
+
...(render && { render: { ...render } }),
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (router.onTimeout) {
|
|
634
|
+
try {
|
|
635
|
+
return await router.onTimeout({
|
|
636
|
+
phase,
|
|
637
|
+
request,
|
|
638
|
+
url,
|
|
639
|
+
env,
|
|
640
|
+
routeKey,
|
|
641
|
+
actionId,
|
|
642
|
+
durationMs,
|
|
643
|
+
...(render && { render: { ...render } }),
|
|
644
|
+
});
|
|
645
|
+
} catch (e) {
|
|
646
|
+
if (process.env.NODE_ENV !== "production") {
|
|
647
|
+
console.error("[RSC] onTimeout callback error:", e);
|
|
648
|
+
}
|
|
649
|
+
return createDefaultTimeoutResponse(phase);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return createDefaultTimeoutResponse(phase);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export interface FinalizeRequestOptions<TEnv> {
|
|
657
|
+
ctx: RequestStageContext<TEnv>;
|
|
658
|
+
reqCtx: RequestContext<TEnv>;
|
|
659
|
+
/** The visitor's request, raw URL and env. */
|
|
660
|
+
request: Request;
|
|
661
|
+
url: URL;
|
|
662
|
+
env: TEnv;
|
|
663
|
+
/** runRequestPasses' result. */
|
|
664
|
+
hasMiddleware: boolean;
|
|
665
|
+
rewrittenTo?: string;
|
|
666
|
+
/** Runs on the response before the open-redirect guard. */
|
|
667
|
+
beforeGuard?: (response: Response) => void;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* The response the global chain returned, made ready to leave: a partial or
|
|
672
|
+
* action request's redirect converted, onResponse callbacks drained, the
|
|
673
|
+
* rewrite header, the open-redirect guard and the stream-idle watchdog.
|
|
674
|
+
*/
|
|
675
|
+
export function finalizeRequestResponse<TEnv>(
|
|
676
|
+
downstream: Response,
|
|
677
|
+
opts: FinalizeRequestOptions<TEnv>,
|
|
678
|
+
): Response {
|
|
679
|
+
const { ctx, reqCtx, request, url, env } = opts;
|
|
680
|
+
const router = ctx.router;
|
|
681
|
+
let response: Response;
|
|
682
|
+
if (opts.hasMiddleware) {
|
|
683
|
+
if (
|
|
684
|
+
url.searchParams.has(WIRE.partial) ||
|
|
685
|
+
url.searchParams.has(WIRE.action)
|
|
686
|
+
) {
|
|
687
|
+
const intercepted = interceptRedirectForPartial(
|
|
688
|
+
downstream,
|
|
689
|
+
ctx.createRedirectFlightResponse,
|
|
690
|
+
{ requestOrigin: url.origin, basename: router.basename },
|
|
691
|
+
);
|
|
692
|
+
response = intercepted ?? finalizeResponse(downstream);
|
|
693
|
+
} else {
|
|
694
|
+
response = finalizeResponse(downstream);
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
response = downstream;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (opts.rewrittenTo !== undefined) {
|
|
701
|
+
trySetHeader(response, REWRITE_HEADER, opts.rewrittenTo);
|
|
702
|
+
}
|
|
703
|
+
opts.beforeGuard?.(response);
|
|
704
|
+
|
|
705
|
+
// Single open-redirect chokepoint: every response (PE, full-page,
|
|
706
|
+
// middleware short-circuit, response-route) funnels through here, so
|
|
707
|
+
// guarding browser-followed (3xx) redirects once covers them all and any
|
|
708
|
+
// future redirect exit. Soft SPA/Flight redirects are 200/204 and pass
|
|
709
|
+
// through untouched (validated client-side instead).
|
|
710
|
+
const guarded = guardOutgoingRedirect(response, url.origin, router.basename);
|
|
711
|
+
|
|
712
|
+
// Stream-idle watchdog (opt-in via timeouts.streamIdleMs): bounds
|
|
713
|
+
// end-to-end idle flow on the streamed body — see rsc/stream-idle.ts
|
|
714
|
+
// for the semantics. Applied at this finalization chokepoint so every
|
|
715
|
+
// streaming exit is covered; websocket upgrades must never be
|
|
716
|
+
// reconstructed and bodiless responses have nothing to bound. The
|
|
717
|
+
// trip fires POST-handoff (the request ALS may be gone), so it
|
|
718
|
+
// reports via the eagerly captured surfaces — callOnError +
|
|
719
|
+
// router.telemetry directly — mirroring handleStore.onError.
|
|
720
|
+
// onTimeout does NOT apply: the response already left the handler,
|
|
721
|
+
// so no replacement Response can be served mid-stream.
|
|
722
|
+
const streamIdleMs = router.timeouts?.streamIdleMs;
|
|
723
|
+
// Websocket check FIRST: a workerd upgrade response must never have
|
|
724
|
+
// its body getter poked.
|
|
725
|
+
if (
|
|
726
|
+
!isTimeoutEnabled(streamIdleMs) ||
|
|
727
|
+
isWebSocketUpgradeResponse(guarded) ||
|
|
728
|
+
!guarded.body
|
|
729
|
+
) {
|
|
730
|
+
return guarded;
|
|
731
|
+
}
|
|
732
|
+
const routeKey = reqCtx._routeName;
|
|
733
|
+
return applyStreamIdleTimeout(guarded, streamIdleMs!, (tripInfo) => {
|
|
734
|
+
ctx.callOnError(tripInfo.error, "handler", {
|
|
735
|
+
request,
|
|
736
|
+
url,
|
|
737
|
+
env,
|
|
738
|
+
routeKey,
|
|
739
|
+
handledByBoundary: false,
|
|
740
|
+
metadata: {
|
|
741
|
+
timeout: true,
|
|
742
|
+
phase: "stream-idle",
|
|
743
|
+
durationMs: tripInfo.totalMs,
|
|
744
|
+
},
|
|
745
|
+
});
|
|
746
|
+
if (router.telemetry) {
|
|
747
|
+
safeEmit(resolveSink(router.telemetry), {
|
|
748
|
+
type: "request.timeout",
|
|
749
|
+
timestamp: performance.now(),
|
|
750
|
+
requestId: getRequestId(request),
|
|
751
|
+
phase: "stream-idle",
|
|
752
|
+
pathname: url.pathname,
|
|
753
|
+
routeKey,
|
|
754
|
+
durationMs: tripInfo.totalMs,
|
|
755
|
+
customHandler: false,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
}
|