@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19
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 +46 -8
- package/dist/bin/rango.js +105 -18
- package/dist/vite/index.js +227 -93
- package/package.json +15 -14
- package/skills/hooks/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +94 -1
- package/skills/middleware/SKILL.md +81 -0
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +187 -17
- package/skills/route/SKILL.md +42 -1
- package/skills/router-setup/SKILL.md +77 -0
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +38 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +25 -27
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +0 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +46 -13
- package/src/browser/navigation-client.ts +32 -61
- package/src/browser/navigation-store.ts +1 -31
- package/src/browser/navigation-transaction.ts +46 -207
- package/src/browser/partial-update.ts +102 -150
- package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
- package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
- package/src/browser/react/Link.tsx +28 -23
- package/src/browser/react/NavigationProvider.tsx +9 -1
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +1 -1
- package/src/browser/react/location-state.ts +2 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/use-action.ts +9 -1
- package/src/browser/react/use-handle.ts +3 -25
- package/src/browser/react/use-params.ts +2 -4
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +1 -1
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +7 -60
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +29 -23
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +115 -96
- package/src/browser/types.ts +1 -31
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +5 -0
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +45 -3
- package/src/build/runtime-discovery.ts +13 -1
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +132 -96
- package/src/cache/cache-scope.ts +71 -73
- package/src/cache/cf/cf-cache-store.ts +9 -4
- package/src/cache/document-cache.ts +72 -47
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/memory-segment-store.ts +18 -7
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +101 -112
- package/src/cache/taint.ts +26 -0
- package/src/client.tsx +53 -30
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +33 -1
- package/src/index.ts +27 -0
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +4 -3
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +94 -15
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +1 -0
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +61 -7
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +69 -4
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/middleware-types.ts +7 -0
- package/src/router/middleware.ts +93 -8
- package/src/router/pattern-matching.ts +41 -5
- package/src/router/prerender-match.ts +34 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +34 -0
- package/src/router/router-options.ts +200 -0
- package/src/router/segment-resolution/fresh.ts +123 -30
- package/src/router/segment-resolution/helpers.ts +19 -0
- package/src/router/segment-resolution/loader-cache.ts +37 -146
- package/src/router/segment-resolution/revalidation.ts +358 -94
- package/src/router/segment-wrappers.ts +3 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/types.ts +7 -1
- package/src/router.ts +155 -11
- package/src/rsc/handler-context.ts +11 -0
- package/src/rsc/handler.ts +380 -88
- package/src/rsc/helpers.ts +25 -16
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +232 -19
- package/src/rsc/response-route-handler.ts +37 -26
- package/src/rsc/rsc-rendering.ts +12 -5
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +134 -58
- package/src/rsc/types.ts +8 -0
- package/src/search-params.ts +22 -10
- package/src/server/context.ts +53 -5
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +66 -9
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +90 -9
- package/src/ssr/index.tsx +63 -27
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +1 -6
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +5 -0
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +9 -0
- package/src/types/handler-context.ts +35 -13
- package/src/types/loader-types.ts +7 -0
- package/src/types/route-entry.ts +28 -0
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +27 -2
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +12 -4
- package/src/vite/discovery/bundle-postprocess.ts +12 -7
- package/src/vite/discovery/discover-routers.ts +30 -18
- package/src/vite/discovery/prerender-collection.ts +24 -27
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/rango.ts +3 -3
- package/src/vite/router-discovery.ts +99 -36
- package/src/vite/utils/prerender-utils.ts +21 -0
- package/src/vite/utils/shared-utils.ts +3 -1
- package/src/browser/request-controller.ts +0 -164
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
package/src/rsc/handler.ts
CHANGED
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
createReverseFunction,
|
|
39
39
|
stripInternalParams,
|
|
40
40
|
} from "../router/handler-context.js";
|
|
41
|
+
import { getRouterContext } from "../router/router-context.js";
|
|
42
|
+
import { resolveSink, safeEmit } from "../router/telemetry.js";
|
|
41
43
|
import { contextSet } from "../context-var.js";
|
|
42
44
|
import {
|
|
43
45
|
hasCachedManifest,
|
|
@@ -50,9 +52,20 @@ import {
|
|
|
50
52
|
import type { HandlerContext } from "./handler-context.js";
|
|
51
53
|
import { buildRouterTrieFromUrlpatterns } from "./manifest-init.js";
|
|
52
54
|
import { handleProgressiveEnhancement } from "./progressive-enhancement.js";
|
|
53
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
executeServerAction,
|
|
57
|
+
revalidateAfterAction,
|
|
58
|
+
type ActionContinuation,
|
|
59
|
+
} from "./server-action.js";
|
|
54
60
|
import { handleLoaderFetch } from "./loader-fetch.js";
|
|
61
|
+
import { checkRequestOrigin, type OriginCheckPhase } from "./origin-guard.js";
|
|
55
62
|
import { handleRscRendering } from "./rsc-rendering.js";
|
|
63
|
+
import {
|
|
64
|
+
withTimeout,
|
|
65
|
+
RouterTimeoutError,
|
|
66
|
+
createDefaultTimeoutResponse,
|
|
67
|
+
type TimeoutPhase,
|
|
68
|
+
} from "../router/timeout.js";
|
|
56
69
|
|
|
57
70
|
/**
|
|
58
71
|
* Create an RSC request handler.
|
|
@@ -109,13 +122,11 @@ export function createRSCHandler<
|
|
|
109
122
|
options.loadSSRModule ??
|
|
110
123
|
(() => import.meta.viteRsc.loadModule("ssr", "index"));
|
|
111
124
|
|
|
112
|
-
// Track errors already reported to onError to prevent double-reporting
|
|
113
|
-
// when errors are caught by a phase-specific handler and re-thrown.
|
|
114
|
-
const reportedErrors = new WeakSet<object>();
|
|
115
|
-
|
|
116
125
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
126
|
+
* Per-request error reporter that deduplicates via the ALS request context.
|
|
127
|
+
*
|
|
128
|
+
* Uses the same _reportedErrors WeakSet as the router layer so errors
|
|
129
|
+
* that propagate across layers are only reported once per request.
|
|
119
130
|
*/
|
|
120
131
|
function callOnError(
|
|
121
132
|
error: unknown,
|
|
@@ -123,6 +134,7 @@ export function createRSCHandler<
|
|
|
123
134
|
context: Parameters<typeof invokeOnError<TEnv>>[3],
|
|
124
135
|
): void {
|
|
125
136
|
if (error != null && typeof error === "object") {
|
|
137
|
+
const reportedErrors = requireRequestContext()._reportedErrors;
|
|
126
138
|
if (reportedErrors.has(error)) return;
|
|
127
139
|
reportedErrors.add(error);
|
|
128
140
|
}
|
|
@@ -139,6 +151,72 @@ export function createRSCHandler<
|
|
|
139
151
|
return routeMap;
|
|
140
152
|
}
|
|
141
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Handle a timeout by reporting the error, emitting telemetry,
|
|
156
|
+
* and returning either the custom onTimeout response or a default 504.
|
|
157
|
+
*/
|
|
158
|
+
async function handleTimeoutResponse(
|
|
159
|
+
request: Request,
|
|
160
|
+
env: TEnv,
|
|
161
|
+
url: URL,
|
|
162
|
+
phase: TimeoutPhase,
|
|
163
|
+
durationMs: number,
|
|
164
|
+
routeKey?: string,
|
|
165
|
+
actionId?: string,
|
|
166
|
+
): Promise<Response> {
|
|
167
|
+
const timeoutError = new RouterTimeoutError(phase, durationMs);
|
|
168
|
+
|
|
169
|
+
callOnError(timeoutError, phase === "action" ? "action" : "handler", {
|
|
170
|
+
request,
|
|
171
|
+
url,
|
|
172
|
+
env,
|
|
173
|
+
routeKey,
|
|
174
|
+
actionId,
|
|
175
|
+
handledByBoundary: false,
|
|
176
|
+
metadata: { timeout: true, phase, durationMs },
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const routerCtx = getRouterContext();
|
|
181
|
+
if (routerCtx?.telemetry) {
|
|
182
|
+
safeEmit(resolveSink(routerCtx.telemetry), {
|
|
183
|
+
type: "request.timeout" as const,
|
|
184
|
+
timestamp: performance.now(),
|
|
185
|
+
requestId: routerCtx.requestId,
|
|
186
|
+
phase,
|
|
187
|
+
pathname: url.pathname,
|
|
188
|
+
routeKey,
|
|
189
|
+
actionId,
|
|
190
|
+
durationMs,
|
|
191
|
+
customHandler: !!router.onTimeout,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
// Router context may not be available
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (router.onTimeout) {
|
|
199
|
+
try {
|
|
200
|
+
return await router.onTimeout({
|
|
201
|
+
phase,
|
|
202
|
+
request,
|
|
203
|
+
url,
|
|
204
|
+
env,
|
|
205
|
+
routeKey,
|
|
206
|
+
actionId,
|
|
207
|
+
durationMs,
|
|
208
|
+
});
|
|
209
|
+
} catch (e) {
|
|
210
|
+
if (process.env.NODE_ENV !== "production") {
|
|
211
|
+
console.error("[RSC] onTimeout callback error:", e);
|
|
212
|
+
}
|
|
213
|
+
return createDefaultTimeoutResponse(phase);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return createDefaultTimeoutResponse(phase);
|
|
218
|
+
}
|
|
219
|
+
|
|
142
220
|
/**
|
|
143
221
|
* Build a 200 Flight response that carries a redirect URL and optional state.
|
|
144
222
|
* Used when a partial/action request results in a redirect -- fetch
|
|
@@ -163,7 +241,8 @@ export function createRSCHandler<
|
|
|
163
241
|
});
|
|
164
242
|
}
|
|
165
243
|
|
|
166
|
-
// Bundle shared dependencies for extracted handler functions
|
|
244
|
+
// Bundle shared dependencies for extracted handler functions.
|
|
245
|
+
// callOnError reads from ALS so it's inherently per-request scoped.
|
|
167
246
|
const handlerCtx: HandlerContext<TEnv> = {
|
|
168
247
|
router,
|
|
169
248
|
version,
|
|
@@ -177,6 +256,11 @@ export function createRSCHandler<
|
|
|
177
256
|
callOnError,
|
|
178
257
|
getRequiredRouteMap,
|
|
179
258
|
createRedirectFlightResponse,
|
|
259
|
+
resolveStreamMode: async (request, env, url) => {
|
|
260
|
+
const resolver = router.ssr?.resolveStreaming;
|
|
261
|
+
if (!resolver) return "stream";
|
|
262
|
+
return resolver({ request, env, url });
|
|
263
|
+
},
|
|
180
264
|
};
|
|
181
265
|
|
|
182
266
|
return async function handler(
|
|
@@ -283,9 +367,6 @@ export function createRSCHandler<
|
|
|
283
367
|
}
|
|
284
368
|
const manifestCacheDur = performance.now() - manifestCacheStart;
|
|
285
369
|
|
|
286
|
-
// Note: Route map for useHref() is loaded lazily via getGlobalRouteMap()
|
|
287
|
-
// This allows it to include all routes from lazy includes after manifest loading
|
|
288
|
-
|
|
289
370
|
// Create unified request context with all methods
|
|
290
371
|
// Includes: stub response, handle store, loader memoization, use(), cookies, headers, cache store
|
|
291
372
|
// params starts empty, populated after route matching via setRequestContextParams
|
|
@@ -296,9 +377,23 @@ export function createRSCHandler<
|
|
|
296
377
|
url,
|
|
297
378
|
variables,
|
|
298
379
|
cacheStore,
|
|
380
|
+
cacheProfiles: router.cacheProfiles,
|
|
299
381
|
executionContext: executionCtx,
|
|
300
382
|
themeConfig: router.themeConfig,
|
|
301
383
|
});
|
|
384
|
+
// Wire background error reporting so "use cache" and other subsystems
|
|
385
|
+
// can surface non-fatal errors through the router's onError callback.
|
|
386
|
+
requestContext._reportBackgroundError = (
|
|
387
|
+
error: unknown,
|
|
388
|
+
category: string,
|
|
389
|
+
) => {
|
|
390
|
+
callOnError(error, "cache", {
|
|
391
|
+
request,
|
|
392
|
+
url,
|
|
393
|
+
metadata: { category },
|
|
394
|
+
});
|
|
395
|
+
};
|
|
396
|
+
|
|
302
397
|
const ctxCreateDur = performance.now() - ctxCreateStart;
|
|
303
398
|
|
|
304
399
|
// Accumulate handler-level timing for Server-Timing header
|
|
@@ -337,7 +432,10 @@ export function createRSCHandler<
|
|
|
337
432
|
createReverseFunction(getRequiredRouteMap()),
|
|
338
433
|
);
|
|
339
434
|
|
|
340
|
-
if (
|
|
435
|
+
if (
|
|
436
|
+
url.searchParams.has("_rsc_partial") ||
|
|
437
|
+
url.searchParams.has("_rsc_action")
|
|
438
|
+
) {
|
|
341
439
|
const intercepted = interceptRedirectForPartial(
|
|
342
440
|
mwResponse,
|
|
343
441
|
createRedirectFlightResponse,
|
|
@@ -360,7 +458,6 @@ export function createRSCHandler<
|
|
|
360
458
|
variables: Record<string, any>,
|
|
361
459
|
nonce: string | undefined,
|
|
362
460
|
): Promise<Response> {
|
|
363
|
-
// First, check for route-level middleware
|
|
364
461
|
const previewStart = performance.now();
|
|
365
462
|
const preview = await router.previewMatch(request, { env });
|
|
366
463
|
const previewDur = performance.now() - previewStart;
|
|
@@ -368,18 +465,209 @@ export function createRSCHandler<
|
|
|
368
465
|
handlerTiming.push(`handler-preview-match;dur=${previewDur.toFixed(2)}`);
|
|
369
466
|
// Response route short-circuit: skip entire RSC pipeline
|
|
370
467
|
if (preview?.responseType && preview.handler) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
468
|
+
const responseOutcome = await withTimeout(
|
|
469
|
+
handleResponseRoute(
|
|
470
|
+
handlerCtx,
|
|
471
|
+
preview as ResponseRouteMatch,
|
|
472
|
+
request,
|
|
473
|
+
env,
|
|
474
|
+
url,
|
|
475
|
+
variables,
|
|
476
|
+
),
|
|
477
|
+
router.timeouts.renderStartMs,
|
|
478
|
+
"render-start",
|
|
479
|
+
);
|
|
480
|
+
if (responseOutcome.timedOut) {
|
|
481
|
+
return handleTimeoutResponse(
|
|
482
|
+
request,
|
|
483
|
+
env,
|
|
484
|
+
url,
|
|
485
|
+
"render-start",
|
|
486
|
+
responseOutcome.durationMs,
|
|
487
|
+
preview?.routeKey,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
return responseOutcome.result;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const routeReverse = createReverseFunction(getRequiredRouteMap());
|
|
494
|
+
|
|
495
|
+
const isAction =
|
|
496
|
+
request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
|
|
497
|
+
const isLoaderFetch = url.searchParams.has("_rsc_loader");
|
|
498
|
+
const actionId =
|
|
499
|
+
request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
|
|
500
|
+
|
|
501
|
+
// Origin guard: reject cross-origin actions, loader fetches, and
|
|
502
|
+
// PE form submissions before any execution. Regular page navigations
|
|
503
|
+
// (GET without _rsc_loader/_rsc_action) are not affected.
|
|
504
|
+
const originPhase: OriginCheckPhase | null = isAction
|
|
505
|
+
? "action"
|
|
506
|
+
: isLoaderFetch
|
|
507
|
+
? "loader"
|
|
508
|
+
: request.method === "POST"
|
|
509
|
+
? "pe-form"
|
|
510
|
+
: null;
|
|
511
|
+
if (originPhase) {
|
|
512
|
+
const originResult = await checkRequestOrigin(
|
|
374
513
|
request,
|
|
514
|
+
url,
|
|
515
|
+
router.originCheck,
|
|
375
516
|
env,
|
|
517
|
+
router.id,
|
|
518
|
+
originPhase,
|
|
519
|
+
);
|
|
520
|
+
if (originResult) {
|
|
521
|
+
const originError = new Error(
|
|
522
|
+
`Origin check rejected: ${request.headers.get("origin") ?? "none"} vs ${request.headers.get("host") ?? "none"}`,
|
|
523
|
+
);
|
|
524
|
+
originError.name = "OriginCheckError";
|
|
525
|
+
|
|
526
|
+
callOnError(originError, "origin", {
|
|
527
|
+
request,
|
|
528
|
+
url,
|
|
529
|
+
env,
|
|
530
|
+
handledByBoundary: false,
|
|
531
|
+
metadata: {
|
|
532
|
+
phase: originPhase,
|
|
533
|
+
origin: request.headers.get("origin"),
|
|
534
|
+
host: request.headers.get("host"),
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
const routerCtx = getRouterContext();
|
|
540
|
+
if (routerCtx?.telemetry) {
|
|
541
|
+
safeEmit(resolveSink(routerCtx.telemetry), {
|
|
542
|
+
type: "request.origin-rejected" as const,
|
|
543
|
+
timestamp: performance.now(),
|
|
544
|
+
requestId: routerCtx.requestId,
|
|
545
|
+
method: request.method,
|
|
546
|
+
pathname: url.pathname,
|
|
547
|
+
phase: originPhase,
|
|
548
|
+
origin: request.headers.get("origin"),
|
|
549
|
+
host: request.headers.get("host"),
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
} catch {
|
|
553
|
+
// Router context may not be available
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return originResult;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Get handle store from request context
|
|
561
|
+
const handleStore = requireRequestContext()._handleStore;
|
|
562
|
+
|
|
563
|
+
// Wire up error reporting for late streaming-handle failures
|
|
564
|
+
// (LateHandlePushError: handle pushed after stream completion).
|
|
565
|
+
// Without this, these errors are only caught by React's error boundary
|
|
566
|
+
// and never reach the router's onError callback or telemetry.
|
|
567
|
+
handleStore.onError = (error: Error) => {
|
|
568
|
+
const reqCtx = requireRequestContext();
|
|
569
|
+
callOnError(error, "handler", {
|
|
570
|
+
request,
|
|
376
571
|
url,
|
|
572
|
+
routeKey: reqCtx._routeName,
|
|
573
|
+
params: reqCtx.params as Record<string, string>,
|
|
574
|
+
handledByBoundary: true,
|
|
575
|
+
});
|
|
576
|
+
try {
|
|
577
|
+
const routerCtx = getRouterContext();
|
|
578
|
+
if (routerCtx?.telemetry) {
|
|
579
|
+
safeEmit(resolveSink(routerCtx.telemetry), {
|
|
580
|
+
type: "handler.error" as const,
|
|
581
|
+
timestamp: performance.now(),
|
|
582
|
+
requestId: routerCtx.requestId,
|
|
583
|
+
error,
|
|
584
|
+
handledByBoundary: true,
|
|
585
|
+
pathname: url.pathname,
|
|
586
|
+
routeKey: reqCtx._routeName,
|
|
587
|
+
params: reqCtx.params as Record<string, string>,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
} catch {
|
|
591
|
+
// Router context may not be available (e.g. prerender path)
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// Set route params early so all execution paths can access ctx.params.
|
|
596
|
+
if (preview?.params) {
|
|
597
|
+
setRequestContextParams(preview.params, preview.routeKey);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Progressive enhancement runs before the normal action/render paths.
|
|
601
|
+
// Route middleware wraps the PE re-render so handlers see the same
|
|
602
|
+
// context variables regardless of JS/no-JS transport.
|
|
603
|
+
const progressiveResult = await handleProgressiveEnhancement(
|
|
604
|
+
handlerCtx,
|
|
605
|
+
request,
|
|
606
|
+
env,
|
|
607
|
+
url,
|
|
608
|
+
isAction,
|
|
609
|
+
handleStore,
|
|
610
|
+
nonce,
|
|
611
|
+
{
|
|
612
|
+
routeMiddleware: preview?.routeMiddleware,
|
|
377
613
|
variables,
|
|
378
|
-
|
|
614
|
+
routeReverse,
|
|
615
|
+
},
|
|
616
|
+
);
|
|
617
|
+
if (progressiveResult) {
|
|
618
|
+
return progressiveResult;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// --- Action execution: runs BEFORE route middleware ---
|
|
622
|
+
// Route middleware wraps rendering only. For actions, the action runs
|
|
623
|
+
// first in the global middleware context, then route middleware wraps
|
|
624
|
+
// the revalidation pass (identical to a normal render).
|
|
625
|
+
let actionContinuation: ActionContinuation | undefined;
|
|
626
|
+
if (isAction && actionId) {
|
|
627
|
+
try {
|
|
628
|
+
const actionOutcome = await withTimeout(
|
|
629
|
+
executeServerAction(
|
|
630
|
+
handlerCtx,
|
|
631
|
+
request,
|
|
632
|
+
env,
|
|
633
|
+
url,
|
|
634
|
+
actionId,
|
|
635
|
+
handleStore,
|
|
636
|
+
),
|
|
637
|
+
router.timeouts.actionMs,
|
|
638
|
+
"action",
|
|
639
|
+
);
|
|
640
|
+
if (actionOutcome.timedOut) {
|
|
641
|
+
return handleTimeoutResponse(
|
|
642
|
+
request,
|
|
643
|
+
env,
|
|
644
|
+
url,
|
|
645
|
+
"action",
|
|
646
|
+
actionOutcome.durationMs,
|
|
647
|
+
preview?.routeKey,
|
|
648
|
+
actionId,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
const result = actionOutcome.result;
|
|
652
|
+
// Response means redirect or error boundary — done.
|
|
653
|
+
if (result instanceof Response) return result;
|
|
654
|
+
actionContinuation = result;
|
|
655
|
+
} catch (error) {
|
|
656
|
+
callOnError(error, "action", {
|
|
657
|
+
request,
|
|
658
|
+
url,
|
|
659
|
+
env,
|
|
660
|
+
actionId,
|
|
661
|
+
handledByBoundary: false,
|
|
662
|
+
});
|
|
663
|
+
console.error(`[RSC] Action error:`, error);
|
|
664
|
+
throw error;
|
|
665
|
+
}
|
|
379
666
|
}
|
|
380
667
|
|
|
381
|
-
//
|
|
382
|
-
|
|
668
|
+
// --- Rendering (action revalidation or navigation) ---
|
|
669
|
+
// Route middleware wraps this — same code path for both cases.
|
|
670
|
+
const renderHandler = async () => {
|
|
383
671
|
const response = await coreRequestHandlerInner(
|
|
384
672
|
request,
|
|
385
673
|
env,
|
|
@@ -388,6 +676,8 @@ export function createRSCHandler<
|
|
|
388
676
|
nonce,
|
|
389
677
|
preview?.params,
|
|
390
678
|
preview?.routeKey,
|
|
679
|
+
handleStore,
|
|
680
|
+
actionContinuation,
|
|
391
681
|
);
|
|
392
682
|
if (preview?.negotiated) {
|
|
393
683
|
response.headers.append("Vary", "Accept");
|
|
@@ -395,32 +685,58 @@ export function createRSCHandler<
|
|
|
395
685
|
return response;
|
|
396
686
|
};
|
|
397
687
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const intercepted = interceptRedirectForPartial(
|
|
410
|
-
mwResponse,
|
|
411
|
-
createRedirectFlightResponse,
|
|
688
|
+
// Wrap the render path (with or without route middleware) in a
|
|
689
|
+
// renderStartMs timeout so slow renders are caught before output.
|
|
690
|
+
const executeRender = async (): Promise<Response> => {
|
|
691
|
+
if (preview?.routeMiddleware && preview.routeMiddleware.length > 0) {
|
|
692
|
+
const mwResponse = await executeMiddleware(
|
|
693
|
+
buildRouteMiddlewareEntries<TEnv>(preview.routeMiddleware),
|
|
694
|
+
request,
|
|
695
|
+
env,
|
|
696
|
+
variables,
|
|
697
|
+
renderHandler,
|
|
698
|
+
routeReverse,
|
|
412
699
|
);
|
|
413
|
-
|
|
700
|
+
|
|
701
|
+
if (
|
|
702
|
+
url.searchParams.has("_rsc_partial") ||
|
|
703
|
+
url.searchParams.has("_rsc_action")
|
|
704
|
+
) {
|
|
705
|
+
const intercepted = interceptRedirectForPartial(
|
|
706
|
+
mwResponse,
|
|
707
|
+
createRedirectFlightResponse,
|
|
708
|
+
);
|
|
709
|
+
if (intercepted) return intercepted;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return finalizeResponse(mwResponse);
|
|
414
713
|
}
|
|
415
714
|
|
|
416
|
-
|
|
417
|
-
|
|
715
|
+
// No route middleware, proceed directly
|
|
716
|
+
return renderHandler();
|
|
717
|
+
};
|
|
418
718
|
|
|
419
|
-
|
|
420
|
-
|
|
719
|
+
const renderOutcome = await withTimeout(
|
|
720
|
+
executeRender(),
|
|
721
|
+
router.timeouts.renderStartMs,
|
|
722
|
+
"render-start",
|
|
723
|
+
);
|
|
724
|
+
if (renderOutcome.timedOut) {
|
|
725
|
+
return handleTimeoutResponse(
|
|
726
|
+
request,
|
|
727
|
+
env,
|
|
728
|
+
url,
|
|
729
|
+
"render-start",
|
|
730
|
+
renderOutcome.durationMs,
|
|
731
|
+
preview?.routeKey,
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
return renderOutcome.result;
|
|
421
735
|
}
|
|
422
736
|
|
|
423
|
-
// Inner request handler
|
|
737
|
+
// Inner request handler: rendering logic wrapped by route middleware.
|
|
738
|
+
// Handles action revalidation (when actionContinuation is present),
|
|
739
|
+
// loader fetches, and regular RSC rendering.
|
|
424
740
|
async function coreRequestHandlerInner(
|
|
425
741
|
request: Request,
|
|
426
742
|
env: TEnv,
|
|
@@ -429,12 +745,12 @@ export function createRSCHandler<
|
|
|
429
745
|
nonce: string | undefined,
|
|
430
746
|
routeParams?: Record<string, string>,
|
|
431
747
|
routeKey?: string,
|
|
748
|
+
handleStore?: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
749
|
+
actionContinuation?: ActionContinuation,
|
|
432
750
|
): Promise<Response> {
|
|
433
751
|
const isPartial = url.searchParams.has("_rsc_partial");
|
|
434
752
|
const isAction =
|
|
435
753
|
request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
|
|
436
|
-
const actionId =
|
|
437
|
-
request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
|
|
438
754
|
|
|
439
755
|
// Version mismatch detection - client may have stale code after HMR/deployment
|
|
440
756
|
// If versions don't match, tell the client to reload
|
|
@@ -500,57 +816,27 @@ export function createRSCHandler<
|
|
|
500
816
|
);
|
|
501
817
|
}
|
|
502
818
|
|
|
503
|
-
|
|
504
|
-
const handleStore = requireRequestContext()._handleStore;
|
|
819
|
+
const store = handleStore ?? requireRequestContext()._handleStore;
|
|
505
820
|
|
|
506
821
|
try {
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
// Previously this was only done for JS actions, leaving PE actions with empty params.
|
|
822
|
+
// Route params were already set in coreRequestHandler, but set again
|
|
823
|
+
// for callers that enter coreRequestHandlerInner directly.
|
|
510
824
|
if (routeParams) {
|
|
511
825
|
setRequestContextParams(routeParams, routeKey);
|
|
512
826
|
}
|
|
513
827
|
|
|
514
828
|
// ============================================================================
|
|
515
|
-
//
|
|
516
|
-
// ============================================================================
|
|
517
|
-
const progressiveResult = await handleProgressiveEnhancement(
|
|
518
|
-
handlerCtx,
|
|
519
|
-
request,
|
|
520
|
-
env,
|
|
521
|
-
url,
|
|
522
|
-
isAction,
|
|
523
|
-
handleStore,
|
|
524
|
-
nonce,
|
|
525
|
-
);
|
|
526
|
-
if (progressiveResult) {
|
|
527
|
-
return progressiveResult;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// ============================================================================
|
|
531
|
-
// SERVER ACTION EXECUTION (JavaScript-enabled client)
|
|
829
|
+
// ACTION REVALIDATION (action already executed, revalidate segments)
|
|
532
830
|
// ============================================================================
|
|
533
|
-
if (
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
);
|
|
543
|
-
} catch (error) {
|
|
544
|
-
callOnError(error, "action", {
|
|
545
|
-
request,
|
|
546
|
-
url,
|
|
547
|
-
env,
|
|
548
|
-
actionId,
|
|
549
|
-
handledByBoundary: false,
|
|
550
|
-
});
|
|
551
|
-
console.error(`[RSC] Action error:`, error);
|
|
552
|
-
throw error;
|
|
553
|
-
}
|
|
831
|
+
if (actionContinuation) {
|
|
832
|
+
return await revalidateAfterAction(
|
|
833
|
+
handlerCtx,
|
|
834
|
+
request,
|
|
835
|
+
env,
|
|
836
|
+
url,
|
|
837
|
+
store,
|
|
838
|
+
actionContinuation,
|
|
839
|
+
);
|
|
554
840
|
}
|
|
555
841
|
|
|
556
842
|
// ============================================================================
|
|
@@ -578,7 +864,7 @@ export function createRSCHandler<
|
|
|
578
864
|
env,
|
|
579
865
|
url,
|
|
580
866
|
isPartial,
|
|
581
|
-
|
|
867
|
+
store,
|
|
582
868
|
nonce,
|
|
583
869
|
);
|
|
584
870
|
} catch (error) {
|
|
@@ -652,7 +938,7 @@ export function createRSCHandler<
|
|
|
652
938
|
diff: [],
|
|
653
939
|
isPartial: false,
|
|
654
940
|
rootLayout: router.rootLayout,
|
|
655
|
-
handles:
|
|
941
|
+
handles: store.stream(),
|
|
656
942
|
version,
|
|
657
943
|
themeConfig: router.themeConfig,
|
|
658
944
|
warmupEnabled: router.warmupEnabled,
|
|
@@ -679,8 +965,14 @@ export function createRSCHandler<
|
|
|
679
965
|
}
|
|
680
966
|
|
|
681
967
|
// Delegate to SSR for HTML response
|
|
682
|
-
const ssrModule = await
|
|
683
|
-
|
|
968
|
+
const [ssrModule, streamMode] = await Promise.all([
|
|
969
|
+
loadSSRModule(),
|
|
970
|
+
handlerCtx.resolveStreamMode(request, env, url),
|
|
971
|
+
]);
|
|
972
|
+
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
973
|
+
nonce,
|
|
974
|
+
streamMode,
|
|
975
|
+
});
|
|
684
976
|
|
|
685
977
|
return createResponseWithMergedHeaders(htmlStream, {
|
|
686
978
|
status: 404,
|
package/src/rsc/helpers.ts
CHANGED
|
@@ -39,7 +39,9 @@ export function createResponseWithMergedHeaders(
|
|
|
39
39
|
return new Response(body, init);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// Merge headers from stub response into the new response
|
|
42
|
+
// Merge headers from stub response into the new response.
|
|
43
|
+
// Delete Set-Cookie from the stub after consuming so that downstream
|
|
44
|
+
// merge points (e.g. executeMiddleware) do not duplicate them.
|
|
43
45
|
const mergedHeaders = new Headers(init.headers);
|
|
44
46
|
ctx.res.headers.forEach((value, name) => {
|
|
45
47
|
if (name.toLowerCase() === "set-cookie") {
|
|
@@ -49,6 +51,7 @@ export function createResponseWithMergedHeaders(
|
|
|
49
51
|
mergedHeaders.set(name, value);
|
|
50
52
|
}
|
|
51
53
|
});
|
|
54
|
+
ctx.res.headers.delete("set-cookie");
|
|
52
55
|
|
|
53
56
|
// Use ctx.res.status if it was set (e.g., 404 for notFound, 500 for error)
|
|
54
57
|
// Otherwise use the status from init
|
|
@@ -86,6 +89,26 @@ export function createSimpleRedirectResponse(redirectUrl: string): Response {
|
|
|
86
89
|
});
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Carry over headers from a source redirect Response to a wrapper Response.
|
|
94
|
+
* Skips Location and X-RSC-Redirect (intentionally replaced by the wrapper)
|
|
95
|
+
* and appends Set-Cookie to avoid clobbering multiple cookie headers.
|
|
96
|
+
*/
|
|
97
|
+
export function carryOverRedirectHeaders(
|
|
98
|
+
source: Response,
|
|
99
|
+
target: Response,
|
|
100
|
+
): void {
|
|
101
|
+
source.headers.forEach((value, name) => {
|
|
102
|
+
const lower = name.toLowerCase();
|
|
103
|
+
if (lower === "location" || lower === "x-rsc-redirect") return;
|
|
104
|
+
if (lower === "set-cookie") {
|
|
105
|
+
target.headers.append(name, value);
|
|
106
|
+
} else if (!target.headers.has(name)) {
|
|
107
|
+
target.headers.set(name, value);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
89
112
|
/**
|
|
90
113
|
* If a response is a 3xx redirect during a partial (client-side) request,
|
|
91
114
|
* intercept it and return a Flight-compatible redirect instead.
|
|
@@ -114,21 +137,7 @@ export function interceptRedirectForPartial(
|
|
|
114
137
|
intercepted = createSimpleRedirectResponse(redirectUrl);
|
|
115
138
|
}
|
|
116
139
|
|
|
117
|
-
|
|
118
|
-
// already on the intercepted response. This preserves cookies and custom
|
|
119
|
-
// headers set by middleware before the redirect.
|
|
120
|
-
response.headers.forEach((value, name) => {
|
|
121
|
-
// Skip redirect-specific and already-handled headers.
|
|
122
|
-
// X-RSC-Redirect from the original 3xx carries "soft" which would
|
|
123
|
-
// collide with the intercepted response's redirect URL or Flight payload.
|
|
124
|
-
const lower = name.toLowerCase();
|
|
125
|
-
if (lower === "location" || lower === "x-rsc-redirect") return;
|
|
126
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
127
|
-
intercepted.headers.append(name, value);
|
|
128
|
-
} else if (!intercepted.headers.has(name)) {
|
|
129
|
-
intercepted.headers.set(name, value);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
140
|
+
carryOverRedirectHeaders(response, intercepted);
|
|
132
141
|
|
|
133
142
|
return intercepted;
|
|
134
143
|
}
|