@rangojs/router 0.27.0 → 0.28.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.
Files changed (212) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/event-controller.d.ts +2 -1
  4. package/dist/types/browser/history-state.d.ts +3 -9
  5. package/dist/types/browser/navigation-store.d.ts +8 -6
  6. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  7. package/dist/types/browser/segment-reconciler.d.ts +6 -4
  8. package/dist/types/browser/types.d.ts +2 -1
  9. package/dist/types/browser/wire-protocol.d.ts +25 -0
  10. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  11. package/dist/types/cache/cache-scope.d.ts +34 -22
  12. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  13. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  14. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  15. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  16. package/dist/types/cache/segment-codec.d.ts +14 -0
  17. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  18. package/dist/types/cache/tag-invalidation.d.ts +10 -0
  19. package/dist/types/cache/taint.d.ts +5 -3
  20. package/dist/types/cache/types.d.ts +7 -3
  21. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  22. package/dist/types/prefetch-markers.d.ts +5 -14
  23. package/dist/types/prerender/store.d.ts +31 -0
  24. package/dist/types/prerender/warm-request.d.ts +3 -3
  25. package/dist/types/prerender.d.ts +2 -2
  26. package/dist/types/route-name.d.ts +5 -0
  27. package/dist/types/router/error-handling.d.ts +4 -4
  28. package/dist/types/router/instrument.d.ts +4 -3
  29. package/dist/types/router/intercept-resolution.d.ts +25 -9
  30. package/dist/types/router/match-context.d.ts +4 -11
  31. package/dist/types/router/match-handlers.d.ts +2 -6
  32. package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
  33. package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
  34. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  35. package/dist/types/router/prerender-match.d.ts +21 -6
  36. package/dist/types/router/preview-match.d.ts +2 -7
  37. package/dist/types/router/request-classification.d.ts +5 -0
  38. package/dist/types/router/router-context.d.ts +7 -46
  39. package/dist/types/router/router-interfaces.d.ts +8 -14
  40. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  41. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  42. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  43. package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
  44. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  45. package/dist/types/router/segment-resolution.d.ts +1 -2
  46. package/dist/types/router/segment-wrappers.d.ts +11 -48
  47. package/dist/types/router/telemetry.d.ts +2 -0
  48. package/dist/types/router/types.d.ts +10 -0
  49. package/dist/types/rsc/full-payload.d.ts +2 -2
  50. package/dist/types/rsc/handler-context.d.ts +6 -6
  51. package/dist/types/rsc/json-route-result.d.ts +5 -9
  52. package/dist/types/rsc/request-stages.d.ts +203 -0
  53. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  54. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  55. package/dist/types/rsc/routine-plan.d.ts +1 -1
  56. package/dist/types/rsc/server-action.d.ts +2 -2
  57. package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
  58. package/dist/types/rsc/shell-capture.d.ts +132 -15
  59. package/dist/types/rsc/shell-serve.d.ts +37 -12
  60. package/dist/types/server/context.d.ts +9 -8
  61. package/dist/types/server/handle-store.d.ts +16 -0
  62. package/dist/types/server/render-pass.d.ts +72 -0
  63. package/dist/types/server/request-context.d.ts +24 -104
  64. package/dist/types/server/rewrite-trail.d.ts +2 -2
  65. package/dist/types/testing/dispatch.d.ts +35 -18
  66. package/dist/types/types/cache-types.d.ts +6 -3
  67. package/dist/types/types/error-types.d.ts +9 -1
  68. package/dist/types/types/handler-context.d.ts +9 -1
  69. package/dist/types/types/segments.d.ts +1 -1
  70. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  71. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  72. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  73. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  74. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  75. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  76. package/dist/types/vite/discovery/state.d.ts +12 -0
  77. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  78. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  79. package/dist/types/vite/router-discovery.d.ts +8 -47
  80. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  81. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  82. package/dist/types/wire-params.d.ts +34 -0
  83. package/dist/vite/index.js +3809 -3710
  84. package/package.json +1 -1
  85. package/skills/cache-guide/SKILL.md +8 -5
  86. package/skills/caching/SKILL.md +37 -18
  87. package/skills/document-cache/SKILL.md +13 -1
  88. package/skills/handler-use/SKILL.md +1 -1
  89. package/skills/intercept/SKILL.md +17 -7
  90. package/skills/middleware/SKILL.md +18 -0
  91. package/skills/ppr/SKILL.md +18 -3
  92. package/skills/prerender/SKILL.md +39 -15
  93. package/skills/rango/SKILL.md +5 -1
  94. package/skills/server-actions/SKILL.md +8 -1
  95. package/skills/testing/response-routes.md +6 -4
  96. package/skills/use-cache/SKILL.md +46 -8
  97. package/src/__internal.ts +7 -1
  98. package/src/browser/connection-warmup.ts +3 -1
  99. package/src/browser/event-controller.ts +5 -3
  100. package/src/browser/history-state.ts +6 -12
  101. package/src/browser/navigation-bridge.ts +14 -13
  102. package/src/browser/navigation-client.ts +18 -25
  103. package/src/browser/navigation-store.ts +25 -27
  104. package/src/browser/navigation-transaction.ts +4 -1
  105. package/src/browser/prefetch/cache.ts +2 -1
  106. package/src/browser/prefetch/fetch.ts +8 -16
  107. package/src/browser/prefetch/queue.ts +4 -4
  108. package/src/browser/react/NavigationProvider.tsx +2 -1
  109. package/src/browser/react/filter-segment-order.ts +2 -2
  110. package/src/browser/rsc-router.tsx +1 -0
  111. package/src/browser/segment-reconciler.ts +66 -53
  112. package/src/browser/server-action-bridge.ts +50 -57
  113. package/src/browser/types.ts +2 -1
  114. package/src/browser/wire-protocol.ts +41 -0
  115. package/src/cache/cache-key-utils.ts +11 -5
  116. package/src/cache/cache-runtime.ts +110 -50
  117. package/src/cache/cache-scope.ts +110 -64
  118. package/src/cache/cf/cf-cache-constants.ts +2 -2
  119. package/src/cache/cf/cf-cache-store.ts +486 -874
  120. package/src/cache/cf/cf-kv-utils.ts +1 -1
  121. package/src/cache/document-cache.ts +34 -13
  122. package/src/cache/handle-snapshot.ts +19 -7
  123. package/src/cache/segment-codec.ts +55 -0
  124. package/src/cache/shell-snapshot.ts +2 -2
  125. package/src/cache/tag-invalidation.ts +21 -2
  126. package/src/cache/taint.ts +5 -3
  127. package/src/cache/types.ts +7 -3
  128. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  129. package/src/handles/pushed-by.ts +1 -1
  130. package/src/prefetch-markers.ts +5 -16
  131. package/src/prerender/store.ts +76 -0
  132. package/src/prerender/warm-request.ts +8 -4
  133. package/src/prerender.ts +2 -2
  134. package/src/route-name.ts +8 -0
  135. package/src/router/error-handling.ts +4 -4
  136. package/src/router/instrument.ts +4 -3
  137. package/src/router/intercept-resolution.ts +77 -75
  138. package/src/router/loader-resolution.ts +5 -4
  139. package/src/router/match-api.ts +4 -5
  140. package/src/router/match-context.ts +20 -12
  141. package/src/router/match-handlers.ts +8 -20
  142. package/src/router/match-middleware/background-revalidation.ts +41 -53
  143. package/src/router/match-middleware/cache-lookup.ts +675 -726
  144. package/src/router/match-middleware/cache-store.ts +4 -8
  145. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  146. package/src/router/match-middleware/segment-resolution.ts +34 -61
  147. package/src/router/middleware.ts +69 -11
  148. package/src/router/navigation-snapshot.ts +4 -4
  149. package/src/router/prerender-match.ts +100 -44
  150. package/src/router/preview-match.ts +6 -41
  151. package/src/router/request-classification.ts +13 -7
  152. package/src/router/route-snapshot.ts +2 -5
  153. package/src/router/router-context.ts +10 -95
  154. package/src/router/router-interfaces.ts +8 -14
  155. package/src/router/segment-resolution/loader-cache.ts +14 -9
  156. package/src/router/segment-resolution/loader-mask.ts +5 -5
  157. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  158. package/src/router/segment-resolution/resolution-pass.ts +137 -0
  159. package/src/router/segment-resolution/revalidation.ts +897 -776
  160. package/src/router/segment-resolution.ts +4 -13
  161. package/src/router/segment-wrappers.ts +51 -269
  162. package/src/router/telemetry.ts +5 -0
  163. package/src/router/types.ts +10 -0
  164. package/src/router.ts +12 -13
  165. package/src/rsc/full-payload.ts +6 -5
  166. package/src/rsc/handler-context.ts +8 -14
  167. package/src/rsc/handler.ts +126 -507
  168. package/src/rsc/json-route-result.ts +5 -9
  169. package/src/rsc/loader-fetch.ts +6 -5
  170. package/src/rsc/progressive-enhancement.ts +5 -0
  171. package/src/rsc/request-stages.ts +757 -0
  172. package/src/rsc/response-cache-serve.ts +10 -14
  173. package/src/rsc/response-route-handler.ts +13 -5
  174. package/src/rsc/rewrite-passes.ts +2 -1
  175. package/src/rsc/routine-plan.ts +1 -1
  176. package/src/rsc/rsc-rendering.ts +164 -349
  177. package/src/rsc/server-action.ts +87 -113
  178. package/src/rsc/shell-capture-constants.ts +1 -1
  179. package/src/rsc/shell-capture.ts +212 -169
  180. package/src/rsc/shell-serve.ts +121 -18
  181. package/src/rsc/ssr-setup.ts +5 -4
  182. package/src/segment-system.tsx +3 -3
  183. package/src/server/context.ts +22 -25
  184. package/src/server/handle-store.ts +55 -0
  185. package/src/server/render-pass.ts +291 -0
  186. package/src/server/request-context.ts +26 -115
  187. package/src/server/rewrite-trail.ts +6 -4
  188. package/src/ssr/index.tsx +1 -1
  189. package/src/testing/dispatch.ts +223 -710
  190. package/src/testing/e2e/page-helpers.ts +2 -2
  191. package/src/testing/serve-shell-request.ts +23 -36
  192. package/src/types/cache-types.ts +6 -3
  193. package/src/types/error-types.ts +9 -1
  194. package/src/types/handler-context.ts +12 -1
  195. package/src/types/segments.ts +1 -1
  196. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  197. package/src/vite/discovery/build-env.ts +148 -0
  198. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  199. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  200. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  201. package/src/vite/discovery/discover-routers.ts +1 -1
  202. package/src/vite/discovery/discovery-errors.ts +1 -1
  203. package/src/vite/discovery/prerender-collection.ts +24 -39
  204. package/src/vite/discovery/state.ts +15 -0
  205. package/src/vite/plugins/version-plugin.ts +1 -1
  206. package/src/vite/plugins/virtual-entries.ts +1 -1
  207. package/src/vite/router-discovery.ts +29 -2428
  208. package/src/vite/temp-rsc-server.ts +223 -0
  209. package/src/vite/utils/prerender-utils.ts +47 -0
  210. package/src/wire-params.ts +35 -0
  211. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  212. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -6,19 +6,15 @@
6
6
  * and progressive enhancement (no-JS form submissions).
7
7
  */
8
8
 
9
- import { requestHeaders } from "../server/request-headers.js";
10
9
  import { isRouteNotFoundError } from "../errors.js";
11
10
  import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
12
11
  import {
13
12
  runWithRequestContext,
14
- setRequestContextParams,
15
13
  getRequestContext,
16
14
  _getRequestContext,
17
15
  createRequestContext,
18
16
  captureRequestEntryContext,
19
17
  } from "../server/request-context.js";
20
- import { REWRITE_HEADER, runRewritePasses } from "./rewrite-passes.js";
21
- import { rewriteTrail } from "../server/rewrite-trail.js";
22
18
  import { payloadLocation } from "./full-payload.js";
23
19
  import * as rscDeps from "@vitejs/plugin-rsc/rsc/server";
24
20
  import type {
@@ -36,7 +32,6 @@ import {
36
32
  buildRouteMiddlewareEntries,
37
33
  } from "./helpers.js";
38
34
  import { renderRscFlightStage, renderRscResponse } from "./render-pipeline.js";
39
- import { guardOutgoingRedirect } from "./redirect-guard.js";
40
35
  import {
41
36
  resolveSoftRedirectUrl,
42
37
  resolveExternalRedirect,
@@ -48,21 +43,13 @@ import {
48
43
  appendVaryAccept,
49
44
  trySetHeader,
50
45
  } from "../response-utils.js";
51
- import {
52
- handleResponseRoute,
53
- type ResponseRouteMatch,
54
- } from "./response-route-handler.js";
55
46
  import { nonce as nonceToken, resolveProviderNonce } from "./nonce.js";
56
47
  import {
57
48
  resolvePrerenderVersion,
58
49
  resolveRouterVersions,
59
50
  } from "../server/build-version-table.js";
60
- import type { ErrorPhase } from "../types.js";
61
51
  import type { RouterRequestInput } from "../router/router-interfaces.js";
62
- import {
63
- invokeOnError,
64
- resolveDefaultNotFound,
65
- } from "../router/error-handling.js";
52
+ import { resolveDefaultNotFound } from "../router/error-handling.js";
66
53
  import {
67
54
  printServerError,
68
55
  wasServerErrorPrinted,
@@ -76,7 +63,6 @@ import {
76
63
  getRouterTrie,
77
64
  } from "../route-map-builder.js";
78
65
  import type { HandlerContext } from "./handler-context.js";
79
- import type { CacheErrorCategory } from "../cache/cache-error.js";
80
66
  import {
81
67
  compileSearchParamsFilter,
82
68
  type SearchParamsFilter,
@@ -91,7 +77,6 @@ import {
91
77
  type ActionContinuation,
92
78
  } from "./server-action.js";
93
79
  import { handleLoaderFetch, renderLoaderError } from "./loader-fetch.js";
94
- import { applyStreamIdleTimeout } from "./stream-idle.js";
95
80
  import {
96
81
  checkRequestOrigin,
97
82
  ORIGIN_CHECK_PHASE_BY_MODE,
@@ -100,14 +85,7 @@ import { handleRscRendering } from "./rsc-rendering.js";
100
85
  import { withoutShellMissMarker } from "./shell-serve.js";
101
86
  import { readWarmMark } from "../prerender/warm-request.js";
102
87
  import { defaultSSRModuleLoaderForTests } from "./ssr-module-loader.js";
103
- import {
104
- withTimeout,
105
- isTimeoutEnabled,
106
- RouterTimeoutError,
107
- createDefaultTimeoutResponse,
108
- type TimeoutPhase,
109
- type RenderTimeoutContext,
110
- } from "../router/timeout.js";
88
+ import { withTimeout, isTimeoutEnabled } from "../router/timeout.js";
111
89
  import {
112
90
  createMetricsStore,
113
91
  appendMetric,
@@ -123,12 +101,21 @@ import {
123
101
  SSR_SETUP_VAR,
124
102
  type SSRSetup,
125
103
  } from "./ssr-setup.js";
104
+ import type { ExecutableRequestPlan } from "../router/request-classification.js";
126
105
  import {
127
- classifyRequest,
128
- type RequestPlan,
129
- type ExecutableRequestPlan,
130
- } from "../router/request-classification.js";
131
- import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
106
+ classify,
107
+ createErrorReporter,
108
+ finalizeRequestResponse,
109
+ originRejectedResponse,
110
+ recordPlanRoute,
111
+ reportBackgroundErrors,
112
+ responseRouteMatch,
113
+ runRequestPasses,
114
+ serveResponseRoute,
115
+ terminalPlanResponse,
116
+ timeoutResponse,
117
+ } from "./request-stages.js";
118
+ import { WIRE } from "../wire-params.js";
132
119
 
133
120
  /**
134
121
  * Create an RSC request handler.
@@ -218,27 +205,7 @@ export function createRSCHandler<
218
205
  }))
219
206
  : rawLoadSSRModule;
220
207
 
221
- /**
222
- * Per-request error reporter that deduplicates via the ALS request context.
223
- *
224
- * Uses the same _reportedErrors WeakSet as the router layer so errors
225
- * that propagate across layers are only reported once per request.
226
- */
227
- function callOnError(
228
- error: unknown,
229
- phase: ErrorPhase,
230
- context: Parameters<typeof invokeOnError<TEnv>>[3],
231
- ): void {
232
- // Guard: abort signal handlers fire asynchronously outside the ALS
233
- // request scope, so the context may be gone. Skip dedup in that
234
- // case — the error is from a cancelled stream, not a real failure.
235
- const reqCtx = _getRequestContext();
236
- if (error != null && typeof error === "object" && reqCtx) {
237
- if (reqCtx._reportedErrors.has(error)) return;
238
- reqCtx._reportedErrors.add(error);
239
- }
240
- invokeOnError(router.onError, error, phase, context, "RSC");
241
- }
208
+ const callOnError = createErrorReporter<TEnv>(router);
242
209
 
243
210
  function getRequiredRouteMap(): Record<string, string> {
244
211
  const routeMap = getRouterManifest(router.id);
@@ -250,112 +217,6 @@ export function createRSCHandler<
250
217
  return routeMap;
251
218
  }
252
219
 
253
- /**
254
- * Handle a timeout by reporting the error, emitting telemetry,
255
- * and returning either the custom onTimeout response or a default 504.
256
- */
257
- async function handleTimeoutResponse(
258
- request: Request,
259
- env: TEnv,
260
- url: URL,
261
- phase: TimeoutPhase,
262
- durationMs: number,
263
- routeKey?: string,
264
- actionId?: string,
265
- ): Promise<Response> {
266
- const timeoutError = new RouterTimeoutError(phase, durationMs);
267
- const requestContext = _getRequestContext<TEnv>();
268
- const cursor = requestContext?._renderForeground;
269
- const render: RenderTimeoutContext | undefined =
270
- phase === "render-start" && cursor
271
- ? {
272
- mode: cursor.mode,
273
- phase: cursor.phase,
274
- state: cursor.state,
275
- completed: cursor.completed,
276
- total: cursor.total,
277
- ...(cursor.phaseStartedAt !== undefined && {
278
- phaseDurationMs: performance.now() - cursor.phaseStartedAt,
279
- }),
280
- }
281
- : undefined;
282
- const trace =
283
- phase === "render-start" ? requestContext?._activeRoutine : undefined;
284
- const activeEntries = trace?.active() ?? [];
285
- const activeStep = activeEntries.at(-1);
286
- const activeAt = performance.now();
287
- const routineSnapshot =
288
- trace && activeStep
289
- ? {
290
- name: trace.name,
291
- path: activeEntries.map((entry) => entry.name),
292
- durationMs: activeAt - activeStep.startedAt,
293
- }
294
- : undefined;
295
-
296
- if (INTERNAL_RANGO_DEBUG && trace && activeStep) {
297
- console.log(
298
- `[routine] TIMEOUT ${request.method} ${url.pathname} (${trace.name})\n${trace.formatActive(activeEntries, activeAt)}`,
299
- );
300
- }
301
-
302
- // Each surface gets its OWN shallow render snapshot. A consumer that mutates
303
- // its copy must not corrupt what the other two observe. The internal-debug
304
- // routine snapshot is bounded metadata sent only to onError.
305
- callOnError(timeoutError, phase === "action" ? "action" : "handler", {
306
- request,
307
- url,
308
- env,
309
- routeKey,
310
- actionId,
311
- handledByBoundary: false,
312
- metadata: {
313
- timeout: true,
314
- phase,
315
- durationMs,
316
- ...(render && { render: { ...render } }),
317
- ...(routineSnapshot && { routine: routineSnapshot }),
318
- },
319
- });
320
-
321
- if (router.telemetry) {
322
- safeEmit(resolveSink(router.telemetry), {
323
- type: "request.timeout",
324
- timestamp: performance.now(),
325
- requestId: getRequestId(request),
326
- phase,
327
- pathname: url.pathname,
328
- routeKey,
329
- actionId,
330
- durationMs,
331
- customHandler: !!router.onTimeout,
332
- ...(render && { render: { ...render } }),
333
- });
334
- }
335
-
336
- if (router.onTimeout) {
337
- try {
338
- return await router.onTimeout({
339
- phase,
340
- request,
341
- url,
342
- env,
343
- routeKey,
344
- actionId,
345
- durationMs,
346
- ...(render && { render: { ...render } }),
347
- });
348
- } catch (e) {
349
- if (process.env.NODE_ENV !== "production") {
350
- console.error("[RSC] onTimeout callback error:", e);
351
- }
352
- return createDefaultTimeoutResponse(phase);
353
- }
354
- }
355
-
356
- return createDefaultTimeoutResponse(phase);
357
- }
358
-
359
220
  /**
360
221
  * Build a 200 Flight response that carries a redirect URL and optional state.
361
222
  * Used when a partial/action request results in a redirect -- fetch
@@ -413,7 +274,7 @@ export function createRSCHandler<
413
274
  env: reqCtx.env,
414
275
  payload: redirectPayload,
415
276
  tracking: {
416
- mode: reqCtx.url.searchParams.has("_rsc_action")
277
+ mode: reqCtx.url.searchParams.has(WIRE.action)
417
278
  ? "action-revalidation"
418
279
  : "partial",
419
280
  routeKey: reqCtx._routeName,
@@ -476,7 +337,7 @@ export function createRSCHandler<
476
337
  // Connection warmup: return 204 immediately before any processing
477
338
  if (router?.warmupEnabled && request.method === "HEAD") {
478
339
  const warmupUrl = new URL(request.url);
479
- if (warmupUrl.searchParams.has("_rsc_warmup")) {
340
+ if (warmupUrl.searchParams.has(WIRE.warmup)) {
480
341
  return new Response(null, { status: 204 });
481
342
  }
482
343
  }
@@ -637,18 +498,7 @@ export function createRSCHandler<
637
498
  requestContext._debugPerformance = true;
638
499
  requestContext._metricsStore = earlyMetricsStore;
639
500
  }
640
- // Wire background error reporting so "use cache" and other subsystems
641
- // can surface non-fatal errors through the router's onError callback.
642
- requestContext._reportBackgroundError = (
643
- error: unknown,
644
- category: CacheErrorCategory,
645
- ) => {
646
- callOnError(error, "cache", {
647
- request,
648
- url,
649
- metadata: { category },
650
- });
651
- };
501
+ reportBackgroundErrors(requestContext, request, url, callOnError);
652
502
 
653
503
  const ctxCreateDur = performance.now() - ctxCreateStart;
654
504
 
@@ -660,9 +510,8 @@ export function createRSCHandler<
660
510
  `handler-ctx-create;dur=${ctxCreateDur.toFixed(2)}`,
661
511
  ];
662
512
 
663
- // Store timing data in variables for downstream access
664
- variables.__handlerTiming = handlerTiming;
665
- variables.__handlerStart = handlerStart;
513
+ // The core handler adds its classify timing (coreRequestHandler).
514
+ requestContext._handlerTiming = handlerTiming;
666
515
 
667
516
  // Wrap entire request handling in request context
668
517
  // Makes context available via getRequestContext() throughout:
@@ -693,57 +542,29 @@ export function createRSCHandler<
693
542
  // latter is reserved for the low-cardinality template (OTel convention).
694
543
  span.setAttribute("url.path", url.pathname);
695
544
 
696
- // One pass per URL: the global middleware chain matched on the pass's
697
- // path, wrapping the core handler. A `throw rewrite()` from global or
698
- // route middleware unwinds the pass, and the next one runs the same
699
- // request as the target from the top, as a direct visit would
700
- // (rsc/rewrite-passes.ts). The response is finalized below, inside the
701
- // response span.
702
- let passIndex = 0;
703
- let hasMiddleware = false;
704
- const { response: downstream, rewrittenTo } = await runRewritePasses({
545
+ // The global middleware passes (rsc/request-stages.ts). The response
546
+ // is finalized below, inside the response span.
547
+ const {
548
+ response: downstream,
549
+ rewrittenTo,
550
+ hasMiddleware,
551
+ } = await runRequestPasses({
552
+ ctx: handlerCtx,
705
553
  reqCtx: requestContext,
706
554
  request,
707
555
  url,
708
- routeMap: getRequiredRouteMap,
709
- basename: router.basename,
710
- runPass: (passRequest, passUrl) => {
711
- const passMiddleware =
712
- passIndex++ === 0
713
- ? matchedMiddleware
714
- : matchMiddleware(
715
- requestContext.url.pathname,
716
- router.middleware,
717
- );
718
- hasMiddleware = passMiddleware.length > 0;
719
- const coreHandler = (): Promise<Response> =>
720
- coreRequestHandler(passRequest, env, passUrl, variables, nonce);
721
- return hasMiddleware
722
- ? executeMiddleware(
723
- passMiddleware,
724
- passRequest,
725
- env,
726
- variables,
727
- coreHandler,
728
- createReverseFunction(getRequiredRouteMap()),
729
- )
730
- : coreHandler();
731
- },
556
+ env,
557
+ variables,
558
+ firstPassMiddleware: matchedMiddleware,
559
+ core: (passRequest, passUrl) =>
560
+ coreRequestHandler(passRequest, env, passUrl, variables, nonce),
732
561
  onHop: (pass) => {
733
562
  span.setAttribute("rango.rewrite.target", pass.target.pathname);
734
563
  span.setAttribute("rango.rewrite.hops", pass.hops);
735
564
  },
736
- report: (error, at) => {
737
- callOnError(error, "routing", {
738
- request: at.request,
739
- url: at.url,
740
- env,
741
- handledByBoundary: false,
742
- });
743
- },
744
565
  // A loader fetch answers with the error `load()` rejects with.
745
566
  refuse: (error) =>
746
- url.searchParams.has("_rsc_loader")
567
+ url.searchParams.has(WIRE.loader)
747
568
  ? renderLoaderError(
748
569
  handlerCtx,
749
570
  { message: (error as Error).message, name: "Error" },
@@ -752,7 +573,7 @@ export function createRSCHandler<
752
573
  request,
753
574
  url,
754
575
  env,
755
- loaderId: url.searchParams.get("_rsc_loader") ?? "",
576
+ loaderId: url.searchParams.get(WIRE.loader) ?? "",
756
577
  },
757
578
  )
758
579
  : undefined,
@@ -767,130 +588,60 @@ export function createRSCHandler<
767
588
  // Header and cookie writes from here on (a streaming loader or
768
589
  // render) no longer reach the response.
769
590
  requestContext._responseSent = true;
770
- let response: Response;
771
- if (hasMiddleware) {
772
- if (
773
- url.searchParams.has("_rsc_partial") ||
774
- url.searchParams.has("_rsc_action")
775
- ) {
776
- const intercepted = interceptRedirectForPartial(
777
- downstream,
778
- createRedirectFlightResponse,
779
- { requestOrigin: url.origin, basename: router.basename },
780
- );
781
- response = intercepted ?? finalizeResponse(downstream);
782
- } else {
783
- response = finalizeResponse(downstream);
784
- }
785
- } else {
786
- response = downstream;
787
- }
788
-
789
- // Finalize metrics after all middleware (including post-next work)
790
- // has completed so :post spans are captured in the timeline.
791
- // Handler timing parts are always emitted (even without debug metrics)
792
- // so non-debug requests still get bootstrap Server-Timing entries.
793
- const handlerTimingArr: string[] = variables.__handlerTiming || [];
794
- // Preserve any existing Server-Timing set by response routes or middleware
795
- const existingTiming = response.headers.get("Server-Timing");
796
- const timingParts = existingTiming
797
- ? [existingTiming, ...handlerTimingArr]
798
- : [...handlerTimingArr];
799
-
800
- const metricsStore = requestContext._metricsStore;
801
- if (metricsStore) {
802
- // When the store was created at handler start (earlyMetricsStore),
803
- // handler:total covers the full request. When ctx.debugPerformance()
804
- // created the store mid-request its requestStart is now the threaded
805
- // _handlerStart (== handlerStart), so both branches yield the true
806
- // request entry; reading the store's own anchor keeps this correct even
807
- // if a store ever lands without the threading (falls back to its start).
808
- const totalStart = earlyMetricsStore
809
- ? handlerStart
810
- : metricsStore.requestStart;
811
- appendMetric(
812
- metricsStore,
813
- "handler:total",
814
- totalStart,
815
- performance.now() - totalStart,
816
- );
817
- const metricsTiming = buildMetricsTiming(
818
- request.method,
819
- url.pathname,
820
- metricsStore,
821
- );
822
- if (metricsTiming) timingParts.push(metricsTiming);
823
- }
824
-
825
- const fullTiming = timingParts.join(", ");
826
- if (fullTiming) trySetHeader(response, "Server-Timing", fullTiming);
827
- if (rewrittenTo !== undefined) {
828
- trySetHeader(response, REWRITE_HEADER, rewrittenTo);
829
- }
830
-
831
- // Single open-redirect chokepoint: every response (PE, full-page,
832
- // middleware short-circuit, response-route) funnels through here, so
833
- // guarding browser-followed (3xx) redirects once covers them all and any
834
- // future redirect exit. Soft SPA/Flight redirects are 200/204 and pass
835
- // through untouched (validated client-side instead).
836
- const guarded = guardOutgoingRedirect(
837
- response,
838
- url.origin,
839
- router.basename,
840
- );
841
-
842
- // Stream-idle watchdog (opt-in via timeouts.streamIdleMs): bounds
843
- // end-to-end idle flow on the streamed body — see rsc/stream-idle.ts
844
- // for the semantics. Applied at this finalization chokepoint so every
845
- // streaming exit is covered; websocket upgrades must never be
846
- // reconstructed and bodiless responses have nothing to bound. The
847
- // trip fires POST-handoff (the request ALS may be gone), so it
848
- // reports via the eagerly captured surfaces — callOnError +
849
- // router.telemetry directly — mirroring handleStore.onError.
850
- // onTimeout does NOT apply: the response already left the handler,
851
- // so no replacement Response can be served mid-stream.
852
- let finalResponse = guarded;
853
- const streamIdleMs = router.timeouts.streamIdleMs;
854
- // Websocket check FIRST: a workerd upgrade response must never have
855
- // its body getter poked (same invariant as the body_kind attribute
856
- // below).
857
- if (
858
- isTimeoutEnabled(streamIdleMs) &&
859
- !isWebSocketUpgradeResponse(guarded) &&
860
- guarded.body
861
- ) {
862
- const routeKey = requestContext._routeName;
863
- finalResponse = applyStreamIdleTimeout(
864
- guarded,
865
- streamIdleMs!,
866
- (tripInfo) => {
867
- callOnError(tripInfo.error, "handler", {
868
- request,
869
- url,
870
- env,
871
- routeKey,
872
- handledByBoundary: false,
873
- metadata: {
874
- timeout: true,
875
- phase: "stream-idle",
876
- durationMs: tripInfo.totalMs,
877
- },
878
- });
879
- if (router.telemetry) {
880
- safeEmit(resolveSink(router.telemetry), {
881
- type: "request.timeout",
882
- timestamp: performance.now(),
883
- requestId: getRequestId(request),
884
- phase: "stream-idle",
885
- pathname: url.pathname,
886
- routeKey,
887
- durationMs: tripInfo.totalMs,
888
- customHandler: false,
889
- });
890
- }
891
- },
892
- );
893
- }
591
+ const finalResponse = finalizeRequestResponse(downstream, {
592
+ ctx: handlerCtx,
593
+ reqCtx: requestContext,
594
+ request,
595
+ url,
596
+ env,
597
+ hasMiddleware,
598
+ rewrittenTo,
599
+ beforeGuard: (response) => {
600
+ // Finalize metrics after all middleware (including post-next
601
+ // work) has completed so :post spans are captured in the
602
+ // timeline. Handler timing parts are always emitted (even
603
+ // without debug metrics) so non-debug requests still get
604
+ // bootstrap Server-Timing entries.
605
+ // Preserve any existing Server-Timing set by response routes or
606
+ // middleware
607
+ const existingTiming = response.headers.get("Server-Timing");
608
+ const timingParts = existingTiming
609
+ ? [existingTiming, ...handlerTiming]
610
+ : [...handlerTiming];
611
+
612
+ const metricsStore = requestContext._metricsStore;
613
+ if (metricsStore) {
614
+ // When the store was created at handler start
615
+ // (earlyMetricsStore), handler:total covers the full request.
616
+ // When ctx.debugPerformance() created the store mid-request
617
+ // its requestStart is now the threaded _handlerStart
618
+ // (== handlerStart), so both branches yield the true request
619
+ // entry; reading the store's own anchor keeps this correct
620
+ // even if a store ever lands without the threading (falls
621
+ // back to its start).
622
+ const totalStart = earlyMetricsStore
623
+ ? handlerStart
624
+ : metricsStore.requestStart;
625
+ appendMetric(
626
+ metricsStore,
627
+ "handler:total",
628
+ totalStart,
629
+ performance.now() - totalStart,
630
+ );
631
+ const metricsTiming = buildMetricsTiming(
632
+ request.method,
633
+ url.pathname,
634
+ metricsStore,
635
+ );
636
+ if (metricsTiming) timingParts.push(metricsTiming);
637
+ }
638
+
639
+ const fullTiming = timingParts.join(", ");
640
+ if (fullTiming) {
641
+ trySetHeader(response, "Server-Timing", fullTiming);
642
+ }
643
+ },
644
+ });
894
645
 
895
646
  // Attributes describe the response actually handed to the host (after
896
647
  // unsafe-redirect replacement), low-cardinality only. body_kind checks
@@ -920,8 +671,9 @@ export function createRSCHandler<
920
671
  };
921
672
 
922
673
  // Core request handling logic (separated for middleware wrapping).
923
- // Uses the classify → execute model: classifyRequest produces a RequestPlan,
924
- // then execution dispatches on the plan mode.
674
+ // The request stages (rsc/request-stages.ts) classify the request into a
675
+ // RequestPlan and answer its terminal plans; execution dispatches on the
676
+ // plan mode.
925
677
  async function coreRequestHandler(
926
678
  request: Request,
927
679
  env: TEnv,
@@ -929,44 +681,12 @@ export function createRSCHandler<
929
681
  variables: Record<string, any>,
930
682
  nonce: string | undefined,
931
683
  ): Promise<Response> {
932
- const handlerTiming: string[] = variables.__handlerTiming || [];
684
+ const reqCtx = getRequestContext();
685
+ const handlerTiming: string[] = reqCtx._handlerTiming || [];
933
686
 
934
687
  // ---- 1. Classify ----
935
- // classifyRequest may throw RouteNotFoundError for unknown routes.
936
- // In that case, fall through to a full-render plan so the pipeline
937
- // can render the 404 page via the existing error handling path.
938
688
  const classifyStart = performance.now();
939
- let plan: RequestPlan<TEnv>;
940
- try {
941
- plan = await classifyRequest<TEnv>(request, url, {
942
- findMatch: router.findMatch,
943
- routerVersion: version,
944
- routerId: router.id,
945
- });
946
- } catch (error) {
947
- if (isRouteNotFoundError(error)) {
948
- // Let the render path handle 404 — match()/matchPartial() will
949
- // re-throw RouteNotFoundError and the catch block in
950
- // executeRenderWithMiddleware renders the not-found page.
951
- plan = {
952
- mode: "full-render",
953
- route: {
954
- matched: null as any,
955
- manifestEntry: null as any,
956
- entries: [],
957
- routeKey: "",
958
- localRouteName: "",
959
- params: {},
960
- routeMiddleware: [],
961
- cacheScope: null,
962
- isPassthrough: false,
963
- },
964
- negotiated: false,
965
- };
966
- } else {
967
- throw error;
968
- }
969
- }
689
+ const plan = await classify(request, url, handlerCtx);
970
690
  const classifyDur = performance.now() - classifyStart;
971
691
  handlerTiming.push(`handler-classify;dur=${classifyDur.toFixed(2)}`);
972
692
 
@@ -974,64 +694,16 @@ export function createRSCHandler<
974
694
  // — the outer handler tail cannot see the plan. Stays unset when middleware
975
695
  // short-circuits before core execution runs (reported as
976
696
  // "middleware-short-circuit").
977
- getRequestContext()._requestMode = plan.mode;
978
-
979
- // A rewritten pass of an action must reach the branch that runs it: the
980
- // one that ran it re-renders from its outcome, and a JS action a global
981
- // rewrite moved before it ran runs only on the action branch. Any other
982
- // plan would serve the POST as a fresh request or drop the action; a
983
- // reload (version mismatch, app switch) does neither.
984
- const passCtx = getRequestContext();
985
- const actionRan = passCtx._actionRan;
986
- const actionMode = actionRan
987
- ? actionRan.kind === "action"
988
- ? "action"
989
- : "pe-render"
990
- : url.searchParams.has("_rsc_action") && rewriteTrail(passCtx)
991
- ? "action"
992
- : undefined;
993
- if (
994
- actionMode &&
995
- plan.mode !== actionMode &&
996
- plan.mode !== "version-mismatch" &&
997
- plan.mode !== "app-switch"
998
- ) {
999
- throw new Error(
1000
- `A rewrite of a server action or form post must target a page ` +
1001
- `route; ${url.pathname} ${plan.mode === "response" ? "is a response route" : plan.mode === "redirect" ? "redirects" : "is not one"}.`,
1002
- );
1003
- }
697
+ reqCtx._requestMode = plan.mode;
1004
698
 
1005
699
  // ---- 2. Terminal plans (no execution needed) ----
1006
- if (plan.mode === "redirect") {
1007
- // Redirects are handled by the pipeline (match/matchPartial),
1008
- // but for partial requests we short-circuit with a Flight redirect.
1009
- if (url.searchParams.has("_rsc_partial")) {
1010
- return createRedirectFlightResponse(plan.redirectUrl);
1011
- }
1012
- // Full requests: let the pipeline handle the redirect via match()
1013
- // which returns { redirect: url }. Fall through to full-render.
1014
- }
1015
-
1016
- // A reload points at the visitor's URL, never a rewrite target's.
1017
- if (plan.mode === "version-mismatch") {
1018
- console.log(
1019
- `[RSC] Version mismatch: client=${url.searchParams.get("_rsc_v")}, server=${version}. Forcing reload.`,
1020
- );
1021
- return createReloadResponse(getRequestContext(), plan.reloadUrl);
1022
- }
1023
-
1024
- if (plan.mode === "app-switch") {
1025
- // Cross-app SPA navigation crossed a host-router app boundary. Force a
1026
- // real document navigation so the target app's document is re-established
1027
- // (stylesheets, theme, warmup, prefetch-TTL). See request-classification.
1028
- return createReloadResponse(getRequestContext(), plan.reloadUrl);
1029
- }
700
+ const terminal = terminalPlanResponse(plan, url, handlerCtx);
701
+ if (terminal) return terminal;
1030
702
 
1031
703
  // ---- 3. Origin guard (gate for action/loader/PE modes) ----
1032
704
  const originPhase = ORIGIN_CHECK_PHASE_BY_MODE[plan.mode];
1033
705
  if (originPhase) {
1034
- const originResult = await checkRequestOrigin(
706
+ const rejection = await checkRequestOrigin(
1035
707
  request,
1036
708
  url,
1037
709
  router.originCheck,
@@ -1039,36 +711,15 @@ export function createRSCHandler<
1039
711
  router.id,
1040
712
  originPhase,
1041
713
  );
1042
- if (originResult) {
1043
- const origin = requestHeaders(request).get("origin");
1044
- const host = requestHeaders(request).get("host");
1045
- const originError = new Error(
1046
- `Origin check rejected: ${origin ?? "none"} vs ${host ?? "none"}`,
1047
- );
1048
- originError.name = "OriginCheckError";
1049
-
1050
- callOnError(originError, "origin", {
714
+ if (rejection) {
715
+ return originRejectedResponse(
716
+ rejection,
717
+ originPhase,
1051
718
  request,
1052
- url,
1053
719
  env,
1054
- handledByBoundary: false,
1055
- metadata: { phase: originPhase, origin, host },
1056
- });
1057
-
1058
- if (router.telemetry) {
1059
- safeEmit(resolveSink(router.telemetry), {
1060
- type: "request.origin-rejected",
1061
- timestamp: performance.now(),
1062
- requestId: getRequestId(request),
1063
- method: request.method,
1064
- pathname: url.pathname,
1065
- phase: originPhase,
1066
- origin,
1067
- host,
1068
- });
1069
- }
1070
-
1071
- return originResult;
720
+ url,
721
+ handlerCtx,
722
+ );
1072
723
  }
1073
724
  }
1074
725
 
@@ -1108,7 +759,9 @@ export function createRSCHandler<
1108
759
  params: reqCtx.params as Record<string, string>,
1109
760
  handledByBoundary: true,
1110
761
  });
1111
- if (router.telemetry) {
762
+ // A PPR shell capture's store inherits this onError
763
+ // (deriveShellCaptureContext); the capture sends nothing to the sink.
764
+ if (router.telemetry && !reqCtx._shellCaptureState?.run) {
1112
765
  safeEmit(resolveSink(router.telemetry), {
1113
766
  type: "handler.error",
1114
767
  timestamp: performance.now(),
@@ -1122,57 +775,21 @@ export function createRSCHandler<
1122
775
  }
1123
776
  };
1124
777
 
1125
- // Set route params early so all execution paths can access ctx.params.
1126
- // Also store the classified snapshot so match/matchPartial can reuse it
1127
- // instead of calling resolveRoute again.
1128
- if (plan.mode !== "redirect") {
1129
- setRequestContextParams(plan.route.params, plan.route.routeKey);
1130
- getRequestContext()._classifiedRoute = plan.route;
1131
- }
778
+ // Before any execution path, so each can read ctx.params.
779
+ recordPlanRoute(plan);
1132
780
 
1133
781
  const routeReverse = createReverseFunction(getRequiredRouteMap());
1134
782
 
1135
783
  // ---- Response route: skip entire RSC pipeline ----
1136
784
  if (plan.mode === "response") {
1137
- // Build ResponseRouteMatch from plan fields. handleResponseRoute
1138
- // expects a flat object with params at the top level.
1139
- const responseMatch: ResponseRouteMatch = {
1140
- responseType: plan.responseType,
1141
- handler: plan.handler,
1142
- params: plan.route.params,
1143
- negotiated: plan.negotiated,
1144
- manifestEntry: plan.manifestEntry,
1145
- routeMiddleware: plan.routeMiddleware,
1146
- };
1147
- const responseOutcome = await withTimeout(
1148
- handleResponseRoute(
1149
- handlerCtx,
1150
- responseMatch,
1151
- request,
1152
- env,
1153
- url,
1154
- variables,
1155
- ),
1156
- router.timeouts.renderStartMs,
1157
- "render-start",
1158
- );
1159
- if (responseOutcome.timedOut) {
1160
- return handleTimeoutResponse(
1161
- request,
1162
- env,
1163
- url,
1164
- "render-start",
1165
- responseOutcome.durationMs,
1166
- plan.route.routeKey,
1167
- );
1168
- }
1169
- const response = responseOutcome.result;
1170
- if (plan.negotiated && !isWebSocketUpgradeResponse(response)) {
1171
- // handleResponseRoute (callHandlerWithVary) already appends Vary: Accept
1172
- // for negotiated responses; dedup so we don't emit Vary: Accept, Accept.
1173
- appendVaryAccept(response);
1174
- }
1175
- return response;
785
+ return serveResponseRoute(handlerCtx, {
786
+ match: responseRouteMatch(plan),
787
+ routeKey: plan.route.routeKey,
788
+ request,
789
+ url,
790
+ env,
791
+ variables,
792
+ });
1176
793
  }
1177
794
 
1178
795
  // SSR setup: kick off in parallel for modes that need HTML rendering.
@@ -1285,7 +902,8 @@ export function createRSCHandler<
1285
902
  "action",
1286
903
  );
1287
904
  if (actionOutcome.timedOut) {
1288
- return handleTimeoutResponse(
905
+ return timeoutResponse(
906
+ handlerCtx,
1289
907
  request,
1290
908
  env,
1291
909
  url,
@@ -1325,7 +943,7 @@ export function createRSCHandler<
1325
943
  // Actions from client-side navigation include _rsc_partial — preserve
1326
944
  // the partial flag so the revalidation returns a Flight stream, not HTML.
1327
945
  // App-switch is already excluded by classifyRequest (would be full-render).
1328
- const isPartialAction = url.searchParams.has("_rsc_partial");
946
+ const isPartialAction = url.searchParams.has(WIRE.partial);
1329
947
  return executeRenderWithMiddleware(
1330
948
  plan.route.routeMiddleware,
1331
949
  plan.negotiated,
@@ -1615,7 +1233,8 @@ export function createRSCHandler<
1615
1233
  "render-start",
1616
1234
  );
1617
1235
  if (renderOutcome.timedOut) {
1618
- return handleTimeoutResponse(
1236
+ return timeoutResponse(
1237
+ handlerCtx,
1619
1238
  request,
1620
1239
  env,
1621
1240
  url,