@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.
Files changed (177) hide show
  1. package/README.md +46 -8
  2. package/dist/bin/rango.js +105 -18
  3. package/dist/vite/index.js +227 -93
  4. package/package.json +15 -14
  5. package/skills/hooks/SKILL.md +1 -1
  6. package/skills/intercept/SKILL.md +79 -0
  7. package/skills/layout/SKILL.md +62 -2
  8. package/skills/loader/SKILL.md +94 -1
  9. package/skills/middleware/SKILL.md +81 -0
  10. package/skills/parallel/SKILL.md +57 -2
  11. package/skills/prerender/SKILL.md +187 -17
  12. package/skills/route/SKILL.md +42 -1
  13. package/skills/router-setup/SKILL.md +77 -0
  14. package/src/__internal.ts +1 -1
  15. package/src/bin/rango.ts +38 -19
  16. package/src/browser/action-coordinator.ts +97 -0
  17. package/src/browser/event-controller.ts +25 -27
  18. package/src/browser/history-state.ts +80 -0
  19. package/src/browser/intercept-utils.ts +1 -1
  20. package/src/browser/link-interceptor.ts +0 -3
  21. package/src/browser/merge-segment-loaders.ts +9 -2
  22. package/src/browser/navigation-bridge.ts +46 -13
  23. package/src/browser/navigation-client.ts +32 -61
  24. package/src/browser/navigation-store.ts +1 -31
  25. package/src/browser/navigation-transaction.ts +46 -207
  26. package/src/browser/partial-update.ts +102 -150
  27. package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
  28. package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
  29. package/src/browser/prefetch/policy.ts +42 -0
  30. package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
  31. package/src/browser/react/Link.tsx +28 -23
  32. package/src/browser/react/NavigationProvider.tsx +9 -1
  33. package/src/browser/react/index.ts +2 -6
  34. package/src/browser/react/location-state-shared.ts +1 -1
  35. package/src/browser/react/location-state.ts +2 -0
  36. package/src/browser/react/nonce-context.ts +23 -0
  37. package/src/browser/react/use-action.ts +9 -1
  38. package/src/browser/react/use-handle.ts +3 -25
  39. package/src/browser/react/use-params.ts +2 -4
  40. package/src/browser/react/use-pathname.ts +2 -3
  41. package/src/browser/react/use-router.ts +1 -1
  42. package/src/browser/react/use-search-params.ts +2 -1
  43. package/src/browser/react/use-segments.ts +7 -60
  44. package/src/browser/response-adapter.ts +73 -0
  45. package/src/browser/rsc-router.tsx +29 -23
  46. package/src/browser/scroll-restoration.ts +10 -7
  47. package/src/browser/server-action-bridge.ts +115 -96
  48. package/src/browser/types.ts +1 -31
  49. package/src/browser/validate-redirect-origin.ts +29 -0
  50. package/src/build/generate-manifest.ts +5 -0
  51. package/src/build/generate-route-types.ts +2 -0
  52. package/src/build/route-types/codegen.ts +13 -4
  53. package/src/build/route-types/include-resolution.ts +13 -0
  54. package/src/build/route-types/per-module-writer.ts +15 -3
  55. package/src/build/route-types/router-processing.ts +45 -3
  56. package/src/build/runtime-discovery.ts +13 -1
  57. package/src/cache/background-task.ts +34 -0
  58. package/src/cache/cache-key-utils.ts +44 -0
  59. package/src/cache/cache-policy.ts +125 -0
  60. package/src/cache/cache-runtime.ts +132 -96
  61. package/src/cache/cache-scope.ts +71 -73
  62. package/src/cache/cf/cf-cache-store.ts +9 -4
  63. package/src/cache/document-cache.ts +72 -47
  64. package/src/cache/handle-capture.ts +81 -0
  65. package/src/cache/memory-segment-store.ts +18 -7
  66. package/src/cache/profile-registry.ts +43 -8
  67. package/src/cache/read-through-swr.ts +134 -0
  68. package/src/cache/segment-codec.ts +101 -112
  69. package/src/cache/taint.ts +26 -0
  70. package/src/client.tsx +53 -30
  71. package/src/errors.ts +6 -1
  72. package/src/handle.ts +1 -1
  73. package/src/handles/MetaTags.tsx +5 -2
  74. package/src/host/cookie-handler.ts +8 -3
  75. package/src/host/router.ts +14 -1
  76. package/src/href-client.ts +3 -1
  77. package/src/index.rsc.ts +33 -1
  78. package/src/index.ts +27 -0
  79. package/src/loader.rsc.ts +12 -4
  80. package/src/loader.ts +8 -0
  81. package/src/prerender/store.ts +4 -3
  82. package/src/prerender.ts +76 -18
  83. package/src/reverse.ts +11 -7
  84. package/src/root-error-boundary.tsx +30 -26
  85. package/src/route-definition/dsl-helpers.ts +9 -6
  86. package/src/route-definition/redirect.ts +15 -3
  87. package/src/route-map-builder.ts +38 -2
  88. package/src/route-name.ts +53 -0
  89. package/src/route-types.ts +7 -0
  90. package/src/router/content-negotiation.ts +1 -1
  91. package/src/router/debug-manifest.ts +16 -3
  92. package/src/router/handler-context.ts +94 -15
  93. package/src/router/intercept-resolution.ts +6 -4
  94. package/src/router/lazy-includes.ts +4 -0
  95. package/src/router/loader-resolution.ts +1 -0
  96. package/src/router/logging.ts +100 -3
  97. package/src/router/manifest.ts +32 -3
  98. package/src/router/match-api.ts +61 -7
  99. package/src/router/match-context.ts +3 -0
  100. package/src/router/match-handlers.ts +185 -11
  101. package/src/router/match-middleware/background-revalidation.ts +65 -85
  102. package/src/router/match-middleware/cache-lookup.ts +69 -4
  103. package/src/router/match-middleware/cache-store.ts +2 -0
  104. package/src/router/match-pipelines.ts +8 -43
  105. package/src/router/middleware-types.ts +7 -0
  106. package/src/router/middleware.ts +93 -8
  107. package/src/router/pattern-matching.ts +41 -5
  108. package/src/router/prerender-match.ts +34 -6
  109. package/src/router/preview-match.ts +7 -1
  110. package/src/router/revalidation.ts +61 -2
  111. package/src/router/router-context.ts +15 -0
  112. package/src/router/router-interfaces.ts +34 -0
  113. package/src/router/router-options.ts +200 -0
  114. package/src/router/segment-resolution/fresh.ts +123 -30
  115. package/src/router/segment-resolution/helpers.ts +19 -0
  116. package/src/router/segment-resolution/loader-cache.ts +37 -146
  117. package/src/router/segment-resolution/revalidation.ts +358 -94
  118. package/src/router/segment-wrappers.ts +3 -0
  119. package/src/router/telemetry-otel.ts +299 -0
  120. package/src/router/telemetry.ts +300 -0
  121. package/src/router/timeout.ts +148 -0
  122. package/src/router/types.ts +7 -1
  123. package/src/router.ts +155 -11
  124. package/src/rsc/handler-context.ts +11 -0
  125. package/src/rsc/handler.ts +380 -88
  126. package/src/rsc/helpers.ts +25 -16
  127. package/src/rsc/loader-fetch.ts +84 -42
  128. package/src/rsc/origin-guard.ts +141 -0
  129. package/src/rsc/progressive-enhancement.ts +232 -19
  130. package/src/rsc/response-route-handler.ts +37 -26
  131. package/src/rsc/rsc-rendering.ts +12 -5
  132. package/src/rsc/runtime-warnings.ts +42 -0
  133. package/src/rsc/server-action.ts +134 -58
  134. package/src/rsc/types.ts +8 -0
  135. package/src/search-params.ts +22 -10
  136. package/src/server/context.ts +53 -5
  137. package/src/server/fetchable-loader-store.ts +11 -6
  138. package/src/server/handle-store.ts +66 -9
  139. package/src/server/loader-registry.ts +11 -46
  140. package/src/server/request-context.ts +90 -9
  141. package/src/ssr/index.tsx +63 -27
  142. package/src/static-handler.ts +7 -0
  143. package/src/theme/ThemeProvider.tsx +6 -1
  144. package/src/theme/index.ts +1 -6
  145. package/src/theme/theme-context.ts +1 -28
  146. package/src/theme/theme-script.ts +2 -1
  147. package/src/types/cache-types.ts +5 -0
  148. package/src/types/error-types.ts +3 -0
  149. package/src/types/global-namespace.ts +9 -0
  150. package/src/types/handler-context.ts +35 -13
  151. package/src/types/loader-types.ts +7 -0
  152. package/src/types/route-entry.ts +28 -0
  153. package/src/urls/include-helper.ts +49 -8
  154. package/src/urls/index.ts +1 -0
  155. package/src/urls/path-helper-types.ts +30 -12
  156. package/src/urls/path-helper.ts +17 -2
  157. package/src/urls/pattern-types.ts +21 -1
  158. package/src/urls/response-types.ts +27 -2
  159. package/src/urls/type-extraction.ts +23 -15
  160. package/src/use-loader.tsx +12 -4
  161. package/src/vite/discovery/bundle-postprocess.ts +12 -7
  162. package/src/vite/discovery/discover-routers.ts +30 -18
  163. package/src/vite/discovery/prerender-collection.ts +24 -27
  164. package/src/vite/discovery/route-types-writer.ts +7 -7
  165. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  166. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  167. package/src/vite/plugins/use-cache-transform.ts +91 -3
  168. package/src/vite/rango.ts +3 -3
  169. package/src/vite/router-discovery.ts +99 -36
  170. package/src/vite/utils/prerender-utils.ts +21 -0
  171. package/src/vite/utils/shared-utils.ts +3 -1
  172. package/src/browser/request-controller.ts +0 -164
  173. package/src/href-context.ts +0 -33
  174. package/src/router.gen.ts +0 -6
  175. package/src/static-handler.gen.ts +0 -5
  176. package/src/urls.gen.ts +0 -8
  177. /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
@@ -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 { handleServerAction } from "./server-action.js";
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
- * Wrapper for invokeOnError that binds the router's onError callback.
118
- * Uses the shared utility from router/error-handling.ts for consistent behavior.
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 (url.searchParams.has("_rsc_partial")) {
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
- return handleResponseRoute(
372
- handlerCtx,
373
- preview as ResponseRouteMatch,
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
- // Wrap RSC handler to append Vary: Accept on content-negotiated routes
382
- const rscHandler = async () => {
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
- if (preview?.routeMiddleware && preview.routeMiddleware.length > 0) {
399
- const mwResponse = await executeMiddleware(
400
- buildRouteMiddlewareEntries<TEnv>(preview.routeMiddleware),
401
- request,
402
- env,
403
- variables,
404
- rscHandler,
405
- createReverseFunction(getRequiredRouteMap()),
406
- );
407
-
408
- if (url.searchParams.has("_rsc_partial")) {
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
- if (intercepted) return intercepted;
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
- return finalizeResponse(mwResponse);
417
- }
715
+ // No route middleware, proceed directly
716
+ return renderHandler();
717
+ };
418
718
 
419
- // No route middleware, proceed directly
420
- return rscHandler();
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 (actual RSC logic, wrapped by route middleware if any)
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
- // Get handle store from request context (created at start of request)
504
- const handleStore = requireRequestContext()._handleStore;
819
+ const store = handleStore ?? requireRequestContext()._handleStore;
505
820
 
506
821
  try {
507
- // Set route params early so all execution paths (progressive enhancement,
508
- // server actions, loader fetches) can access ctx.params via getRequestContext().
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
- // PROGRESSIVE ENHANCEMENT: No-JS Form Submissions
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 (isAction && actionId) {
534
- try {
535
- return await handleServerAction(
536
- handlerCtx,
537
- request,
538
- env,
539
- url,
540
- actionId,
541
- handleStore,
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
- handleStore,
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: handleStore.stream(),
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 loadSSRModule();
683
- const htmlStream = await ssrModule.renderHTML(rscStream, { nonce });
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,
@@ -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
- // Carry over headers from the original redirect response that are not
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
  }