vinext 1.4.0 → 1.4.1

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 (104) hide show
  1. package/README.md +20 -20
  2. package/dist/build/prerender.js +3 -2
  3. package/dist/check.js +3 -6
  4. package/dist/client/dev-error-overlay-store.d.ts +1 -0
  5. package/dist/client/dev-error-overlay.d.ts +2 -0
  6. package/dist/client/dev-error-overlay.js +19 -3
  7. package/dist/config/config-matchers.js +2 -0
  8. package/dist/config/destination-params.d.ts +2 -1
  9. package/dist/config/destination-params.js +52 -2
  10. package/dist/config/next-config.d.ts +6 -0
  11. package/dist/config/next-config.js +18 -3
  12. package/dist/entries/app-rsc-entry.js +0 -1
  13. package/dist/entries/pages-server-entry.js +3 -1
  14. package/dist/index.js +29 -20
  15. package/dist/plugins/strip-server-exports.js +2 -1
  16. package/dist/server/api-handler.js +2 -1
  17. package/dist/server/app-browser-entry.js +7 -3
  18. package/dist/server/app-browser-rsc-redirect.js +3 -3
  19. package/dist/server/app-browser-server-action-client.js +2 -2
  20. package/dist/server/app-browser-visible-commit.js +2 -2
  21. package/dist/server/app-optimistic-routing.js +2 -3
  22. package/dist/server/app-page-boundary-render.js +5 -10
  23. package/dist/server/app-page-dispatch.d.ts +0 -1
  24. package/dist/server/app-page-dispatch.js +0 -1
  25. package/dist/server/app-page-element-builder.js +3 -3
  26. package/dist/server/app-page-execution.js +9 -6
  27. package/dist/server/app-page-render.d.ts +0 -1
  28. package/dist/server/app-page-render.js +14 -14
  29. package/dist/server/app-page-response.js +1 -1
  30. package/dist/server/app-page-route-wiring.js +22 -5
  31. package/dist/server/app-page-stream.d.ts +10 -8
  32. package/dist/server/app-page-stream.js +2 -1
  33. package/dist/server/app-request-stage-independent-entry.js +1 -1
  34. package/dist/server/app-route-handler-execution.js +3 -2
  35. package/dist/server/app-route-handler-runtime.d.ts +1 -1
  36. package/dist/server/app-route-handler-runtime.js +11 -7
  37. package/dist/server/app-router-entry.js +1 -1
  38. package/dist/server/app-rsc-cache-busting.d.ts +10 -10
  39. package/dist/server/app-rsc-cache-busting.js +27 -68
  40. package/dist/server/app-rsc-error-handler.js +1 -1
  41. package/dist/server/app-rsc-errors.js +4 -1
  42. package/dist/server/app-rsc-handler.js +53 -62
  43. package/dist/server/app-rsc-request-normalization.d.ts +10 -12
  44. package/dist/server/app-rsc-request-normalization.js +11 -14
  45. package/dist/server/app-rsc-response-stage.js +4 -12
  46. package/dist/server/app-server-action-execution.js +8 -4
  47. package/dist/server/app-ssr-entry.d.ts +6 -0
  48. package/dist/server/app-ssr-entry.js +13 -8
  49. package/dist/server/app-worker-stages.d.ts +12 -3
  50. package/dist/server/app-worker-stages.js +37 -4
  51. package/dist/server/bfcache-identity.d.ts +7 -0
  52. package/dist/server/bfcache-identity.js +10 -1
  53. package/dist/server/cacheability-manifest.js +1 -2
  54. package/dist/server/dev-origin-check.d.ts +2 -1
  55. package/dist/server/dev-origin-check.js +2 -1
  56. package/dist/server/dev-server.js +2 -1
  57. package/dist/server/image-optimization.d.ts +5 -0
  58. package/dist/server/image-optimization.js +6 -1
  59. package/dist/server/isr-cache.js +4 -1
  60. package/dist/server/metadata-route-response.js +1 -1
  61. package/dist/server/middleware-matcher.js +19 -6
  62. package/dist/server/middleware-runtime.js +6 -2
  63. package/dist/server/multi-stage.d.ts +7 -3
  64. package/dist/server/pages-node-compat.d.ts +1 -1
  65. package/dist/server/pages-node-compat.js +2 -0
  66. package/dist/server/pages-page-data.js +2 -1
  67. package/dist/server/pages-page-handler.js +17 -6
  68. package/dist/server/pages-page-response.d.ts +9 -0
  69. package/dist/server/pages-page-response.js +18 -4
  70. package/dist/server/pages-request-stage-entry.js +24 -4
  71. package/dist/server/pages-router-entry.js +3 -2
  72. package/dist/server/prod-server.d.ts +2 -2
  73. package/dist/server/prod-server.js +42 -8
  74. package/dist/server/request-error-log.d.ts +9 -0
  75. package/dist/server/request-error-log.js +13 -0
  76. package/dist/server/request-pipeline.d.ts +9 -4
  77. package/dist/server/request-pipeline.js +23 -15
  78. package/dist/server/rsc-completion-metadata.js +3 -0
  79. package/dist/server/static-file-cache.d.ts +9 -0
  80. package/dist/server/static-file-cache.js +13 -2
  81. package/dist/shims/cache-request-state.d.ts +6 -6
  82. package/dist/shims/cache-request-state.js +6 -36
  83. package/dist/shims/cache-runtime.d.ts +3 -1
  84. package/dist/shims/cache-runtime.js +77 -47
  85. package/dist/shims/cache.d.ts +10 -5
  86. package/dist/shims/cache.js +35 -22
  87. package/dist/shims/client-hook-error.d.ts +2 -0
  88. package/dist/shims/client-hook-error.js +16 -1
  89. package/dist/shims/fetch-cache.js +6 -2
  90. package/dist/shims/headers.d.ts +2 -1
  91. package/dist/shims/headers.js +5 -3
  92. package/dist/shims/internal/cache-context-accessor.d.ts +1 -1
  93. package/dist/shims/link.js +5 -10
  94. package/dist/shims/navigation.js +2 -3
  95. package/dist/shims/request-state-types.d.ts +2 -1
  96. package/dist/shims/slot.d.ts +6 -2
  97. package/dist/shims/slot.js +10 -3
  98. package/dist/shims/unified-request-context.d.ts +6 -0
  99. package/dist/shims/unified-request-context.js +1 -0
  100. package/dist/utils/cache-life-profiles.d.ts +33 -0
  101. package/dist/utils/cache-life-profiles.js +111 -0
  102. package/dist/utils/middleware-request-headers.d.ts +14 -3
  103. package/dist/utils/middleware-request-headers.js +54 -7
  104. package/package.json +1 -1
@@ -8,7 +8,7 @@ import { normalizePathnameForRouteMatchStrict } from "../routing/utils.js";
8
8
  import { normalizePath } from "./normalize-path.js";
9
9
  import { normalizeMountedSlotsHeader } from "./app-mounted-slots-header.js";
10
10
  import { APP_RSC_RENDER_MODE_NAVIGATION, APP_RSC_RENDER_MODE_PREFETCH_EMPTY, APP_RSC_RENDER_MODE_PREFETCH_LOADING_SHELL, parseAppRscRenderMode } from "./app-rsc-render-mode.js";
11
- import { VINEXT_RSC_CACHE_BUSTING_SEARCH_PARAM, stripRscSuffix } from "./app-rsc-cache-busting.js";
11
+ import { VINEXT_RSC_CACHE_BUSTING_SEARCH_PARAM } from "./app-rsc-cache-busting.js";
12
12
  import { normalizeInterceptionContextHeader } from "./app-interception-context-header.js";
13
13
  //#region src/server/app-rsc-request-normalization.ts
14
14
  function extractFlightRouterStatePath(value, depth = 0) {
@@ -78,13 +78,13 @@ function tryNormalizeComparablePathAndSearch(value, basePath, baseUrl) {
78
78
  * 4. Collapse double-slashes, resolve `.` and `..` segments (normalizePath)
79
79
  * 5. basePath check + strip — 404 when pathname lacks the basePath prefix.
80
80
  * `/__vinext/` bypasses this for internal prerender endpoints.
81
- * 6. RSC detection: `.rsc` suffix or Next-style `RSC: 1`. The internal
82
- * `_rsc` cache-busting query is validated separately so full-route Flight
83
- * responses do not share the canonical HTML URL in caches that ignore Vary.
84
- * 7. cleanPathname — pathname with `.rsc` suffix stripped
85
- * 8. Sanitize X-Vinext-Interception-Context — strip null bytes (header injection)
86
- * 9. Normalize x-vinext-mounted-slots — dedup and sort for canonical cache keys
87
- * 10. Read semantic render mode for refresh/action payload rendering
81
+ * 6. RSC detection: `RSC: 1`, as in Next.js. A `.rsc` suffix is an ordinary
82
+ * part of the pathname. The internal `_rsc` cache-busting query is
83
+ * validated separately so full-route Flight responses do not share the
84
+ * canonical HTML URL in caches that ignore Vary.
85
+ * 7. Sanitize X-Vinext-Interception-Context — strip null bytes (header injection)
86
+ * 8. Normalize x-vinext-mounted-slots — dedup and sort for canonical cache keys
87
+ * 9. Read semantic render mode for refresh/action payload rendering
88
88
  *
89
89
  * @returns A 400 or 404 Response for invalid or out-of-scope inputs,
90
90
  * or a NormalizedRscRequest for valid requests.
@@ -110,9 +110,7 @@ function normalizeRscRequest(request, basePath, allowOutsideBasePath = false, as
110
110
  requestPathname = stripBasePath(requestPathname, basePath);
111
111
  }
112
112
  }
113
- const isRscRequest = pathname.endsWith(".rsc") || request.headers.get("RSC") === "1";
114
- const cleanPathname = stripRscSuffix(pathname);
115
- const requestCleanPathname = stripRscSuffix(requestPathname);
113
+ const isRscRequest = request.headers.get("RSC") === "1";
116
114
  const interceptionContextHeader = normalizeInterceptionContextHeader(request.headers.get(VINEXT_INTERCEPTION_CONTEXT_HEADER));
117
115
  const rawInterceptionId = request.headers.get(VINEXT_INTERCEPTION_ID_HEADER);
118
116
  if (rawInterceptionId !== null && (rawInterceptionId.length > 4096 || !rawInterceptionId.startsWith("interception:"))) return badRequestResponse();
@@ -124,7 +122,7 @@ function normalizeRscRequest(request, basePath, allowOutsideBasePath = false, as
124
122
  const routerState = parsePrefetchRouterState(request.headers.get(NEXT_ROUTER_STATE_TREE_HEADER));
125
123
  if (nextUrl && routerState) {
126
124
  const targetUrl = new URL(url);
127
- targetUrl.pathname = cleanPathname;
125
+ targetUrl.pathname = pathname;
128
126
  targetUrl.searchParams.delete(VINEXT_RSC_CACHE_BUSTING_SEARCH_PARAM);
129
127
  const routerPathAndSearch = tryNormalizeComparablePathAndSearch(routerState.pathAndSearch, basePath, url);
130
128
  const nextPathAndSearch = tryNormalizeComparablePathAndSearch(nextUrl, basePath, url);
@@ -136,8 +134,7 @@ function normalizeRscRequest(request, basePath, allowOutsideBasePath = false, as
136
134
  hadBasePath,
137
135
  url,
138
136
  pathname,
139
- cleanPathname,
140
- requestCleanPathname,
137
+ requestPathname,
141
138
  isRscRequest,
142
139
  interceptionContextHeader,
143
140
  interceptionIdHeader,
@@ -10,7 +10,7 @@ import { applyEffectiveRequestCookieHeader, getDraftModeCookieHeader, headersCon
10
10
  import { ensureFetchPatch, setCurrentFetchSoftTags } from "../shims/fetch-cache.js";
11
11
  import { traceFindPageComponents } from "./pages-execution-tracing.js";
12
12
  import { getRenderedConcreteUrlPathsForRoute } from "./pregenerated-concrete-paths.js";
13
- import { VINEXT_RSC_CACHE_BUSTING_SEARCH_PARAM, hasRscCacheBustingSearchParam, stripRscCacheBustingSearchParam, stripRscSuffix } from "./app-rsc-cache-busting.js";
13
+ import { VINEXT_RSC_CACHE_BUSTING_SEARCH_PARAM, hasRscCacheBustingSearchParam, stripRscCacheBustingSearchParam } from "./app-rsc-cache-busting.js";
14
14
  import { traceAppPageRender } from "./app-page-tracing.js";
15
15
  import { buildPageCacheTags } from "./implicit-tags.js";
16
16
  import { pickRootParams, setRootParams } from "../shims/root-params.js";
@@ -48,13 +48,6 @@ function requestWithoutRscCacheBustingSearchParam(request) {
48
48
  stripRscCacheBustingSearchParam(url);
49
49
  return cloneRequestWithUrl(request, url.toString());
50
50
  }
51
- function requestWithoutRscSuffix(request) {
52
- const url = new URL(request.url);
53
- const pathname = stripRscSuffix(url.pathname);
54
- if (pathname === url.pathname) return request;
55
- url.pathname = pathname;
56
- return cloneRequestWithUrl(request, url.toString());
57
- }
58
51
  function pathnameForResolvedUrl(resolvedUrl) {
59
52
  return resolvedUrl.split("#", 1)[0].split("?", 1)[0];
60
53
  }
@@ -192,9 +185,8 @@ async function renderAppWorkerResponseStage(options, rawRequest, ctx, props, sta
192
185
  setRootParams(rootParams);
193
186
  if (props.kind === "app-route-handler") {
194
187
  setCurrentFetchSoftTags(buildPageCacheTags(props.cleanPathname, [], [...route.routeSegments], "route"));
195
- const normalizedUserlandRequest = requestWithoutRscSuffix(request);
196
- const userlandRequest = requestWithoutRscCacheBustingSearchParam(normalizedUserlandRequest);
197
- const routeHandlerRequest = isEdgeRouteHandler(route.routeHandler) ? userlandRequest : normalizedUserlandRequest;
188
+ const userlandRequest = requestWithoutRscCacheBustingSearchParam(request);
189
+ const routeHandlerRequest = isEdgeRouteHandler(route.routeHandler) ? userlandRequest : request;
198
190
  const routeHandlerUrl = new URL(routeHandlerRequest.url);
199
191
  const internalRscValues = isEdgeRouteHandler(route.routeHandler) ? [] : routeHandlerUrl.searchParams.getAll(VINEXT_RSC_CACHE_BUSTING_SEARCH_PARAM);
200
192
  routeHandlerUrl.search = searchParams.toString();
@@ -225,7 +217,7 @@ async function renderAppWorkerResponseStage(options, rawRequest, ctx, props, sta
225
217
  handlerStart: process.env.NODE_ENV !== "production" ? performance.now() : 0,
226
218
  interceptionContext: normalized.interceptionContextHeader,
227
219
  interceptionId: normalized.interceptionIdHeader,
228
- interceptionPathname: props.matchKind === "resolved" ? props.cleanPathname : normalized.requestCleanPathname,
220
+ interceptionPathname: props.matchKind === "resolved" ? props.cleanPathname : normalized.requestPathname,
229
221
  isProgressiveActionRender: false,
230
222
  isRscRequest: normalized.isRscRequest,
231
223
  middlewareContext,
@@ -11,6 +11,7 @@ import { internalServerErrorResponse, payloadTooLargeResponse } from "./http-err
11
11
  import { attachRequestCfMetadata, validateCsrfOrigin, validateServerActionPayload } from "./request-pipeline.js";
12
12
  import { splitPathSegments } from "../routing/utils.js";
13
13
  import { getRequestExecutionContext } from "../shims/request-context.js";
14
+ import { logRequestError } from "./request-error-log.js";
14
15
  import { shareIsolatePromise } from "./isolate-shared-promise.js";
15
16
  import { APP_RSC_RENDER_MODE_NAVIGATION } from "./app-rsc-render-mode.js";
16
17
  import { isDraftModeRequest, setHeadersContext } from "../shims/headers.js";
@@ -509,7 +510,10 @@ function createRejectedActionResult(error) {
509
510
  * Next.js, a thrown string is not logged in development.
510
511
  */
511
512
  function logRejectedActionResult(error) {
512
- if (process.env.NODE_ENV === "production" || hasDigest(error)) console.error("[vinext] Server action error:", error);
513
+ if (process.env.NODE_ENV === "production" || hasDigest(error)) {
514
+ if (typeof error === "string") console.error("[vinext] Server action error:", error);
515
+ else logRequestError("[vinext] Server action error:", error);
516
+ }
513
517
  }
514
518
  function getActionHttpFallbackStatus(error) {
515
519
  const digest = getNextErrorDigest(error);
@@ -520,7 +524,7 @@ function getActionHttpFallbackStatus(error) {
520
524
  }
521
525
  function createServerActionErrorResponse(error, options) {
522
526
  options.getAndClearPendingCookies();
523
- console.error("[vinext] Server action error:", error);
527
+ logRequestError("[vinext] Server action error:", error);
524
528
  options.reportRequestError(error, {
525
529
  path: options.cleanPathname,
526
530
  method: options.request.method,
@@ -627,7 +631,7 @@ async function handleProgressiveServerActionRequest(options) {
627
631
  actionFailed = true;
628
632
  actionFailedWithError = getActionHttpFallbackStatus(error) === null && !isServerActionNotFoundError(error, null);
629
633
  if (actionFailedWithError) {
630
- console.error("[vinext] Server action error:", error);
634
+ logRequestError("[vinext] Server action error:", error);
631
635
  options.reportRequestError(error, {
632
636
  path: options.cleanPathname,
633
637
  method: options.request.method,
@@ -699,7 +703,7 @@ async function handleProgressiveServerActionRequest(options) {
699
703
  });
700
704
  getAndClearActionRevalidationKind();
701
705
  options.getAndClearPendingCookies();
702
- console.error("[vinext] Server action payload parsing error:", error);
706
+ logRequestError("[vinext] Server action payload parsing error:", error);
703
707
  options.reportRequestError(error, {
704
708
  path: options.cleanPathname,
705
709
  method: options.request.method,
@@ -72,6 +72,12 @@ export declare function handleSsr(rscStream: ReadableStream<Uint8Array>, navCont
72
72
  */
73
73
  isCacheCandidate?: boolean;
74
74
  fallbackToErrorDocumentOnShellError?: boolean;
75
+ /**
76
+ * Whether the RSC render threw the given shell error. As in Next.js, in
77
+ * dev the error document carries only an error that the HTML render alone
78
+ * threw, for the overlay: the browser reports the others itself.
79
+ */
80
+ isServerComponentError?: (error: unknown) => boolean;
75
81
  /**
76
82
  * The parallel slot not-found boundaries that catch the error that
77
83
  * rejected the shell, or null when none catches it. The document then
@@ -2,6 +2,7 @@ import { deferUntilStreamConsumed } from "./defer-until-stream-consumed.js";
2
2
  import { notFoundResponse } from "./http-error-responses.js";
3
3
  import { isOpenRedirectShaped, repeatedSlashRedirectResponse } from "./open-redirect.js";
4
4
  import "./server-globals.js";
5
+ import { isNavigationSignalError } from "../utils/navigation-signal.js";
5
6
  import { isPprFallbackShellAbortError } from "../shims/ppr-fallback-shell.js";
6
7
  import { onRenderDynamicLatched } from "../shims/internal/headers-state.js";
7
8
  import { AppRouterContext } from "../shims/internal/app-router-context.js";
@@ -15,6 +16,7 @@ import { createNavigationRuntimeRenderedSearchScript, createNavigationRuntimeRsc
15
16
  import { AppElementsWire } from "./app-elements-wire.js";
16
17
  import "./app-elements.js";
17
18
  import { createInitialBfcacheMaps } from "./app-bfcache-identity.js";
19
+ import { isBailoutToCSRError, isDynamicServerError } from "../shims/navigation-errors.js";
18
20
  import { ServerInsertedHTMLContext, clearServerInsertedHTML, getBfcacheIdMapContext, registerServerInsertedHTMLCallback, renderServerInsertedHTML, setNavigationContext } from "../shims/navigation-context-state.js";
19
21
  import "../shims/navigation-server.js";
20
22
  import { BfcacheIdentityMapContext } from "../shims/slot.js";
@@ -71,13 +73,14 @@ function createUtf8Stream(html) {
71
73
  controller.close();
72
74
  } });
73
75
  }
74
- function buildBootstrapModuleScript(bootstrapModuleUrl, nonce) {
76
+ function buildBootstrapModuleScript(bootstrapModuleUrl, nonce, crossOrigin) {
75
77
  if (!bootstrapModuleUrl) return "";
76
- return `<script type="module"${createNonceAttribute(nonce)} src="` + escapeHtmlAttr(bootstrapModuleUrl) + "\" id=\"_R_\" async=\"\" fetchPriority=\"low\"><\/script>";
78
+ const crossOriginAttribute = crossOrigin === void 0 ? "" : ` crossorigin="${crossOrigin === "use-credentials" ? "use-credentials" : ""}"`;
79
+ return `<script type="module"${createNonceAttribute(nonce)} src="` + escapeHtmlAttr(bootstrapModuleUrl) + `"${crossOriginAttribute} id="_R_" async="" fetchPriority="low"><\/script>`;
77
80
  }
78
- function renderSsrErrorDocumentShell(bootstrapModuleUrl, nonce) {
79
- const html = renderToStaticMarkup(createElement(DefaultGlobalError, { error: null })).replace("<style>", "<style data-vinext-error-shell-style=\"\">");
80
- const bootstrapScript = buildBootstrapModuleScript(bootstrapModuleUrl, nonce);
81
+ function renderSsrErrorDocumentShell(bootstrapModuleUrl, nonce, crossOrigin) {
82
+ const html = renderToStaticMarkup(createElement(DefaultGlobalError, { error: null })).replace("<head>", "<head><meta name=\"robots\" content=\"noindex\"/>").replace("<style>", "<style data-vinext-error-shell-style=\"\">");
83
+ const bootstrapScript = buildBootstrapModuleScript(bootstrapModuleUrl, nonce, crossOrigin);
81
84
  if (!bootstrapScript) return createUtf8Stream(`<!DOCTYPE html>${html}`);
82
85
  const documentClose = "</body></html>";
83
86
  if (!html.endsWith(documentClose)) return createUtf8Stream(`<!DOCTYPE html>${html}${bootstrapScript}`);
@@ -292,6 +295,7 @@ async function handleSsr(rscStream, navContext, fontData, options) {
292
295
  let htmlStream;
293
296
  let renderComplete = Promise.resolve();
294
297
  let shellErrorRecovered = false;
298
+ let ssrOnlyShellError;
295
299
  let caughtShellError;
296
300
  let caughtBoundaries = null;
297
301
  let shouldDelayInitialHtmlPull = false;
@@ -355,10 +359,11 @@ async function handleSsr(rscStream, navContext, fontData, options) {
355
359
  shouldDelayInitialHtmlPull = options?.waitForAllReady !== true;
356
360
  caughtShellError = error;
357
361
  htmlStream = caughtHtmlStream;
358
- } else if (options?.fallbackToErrorDocumentOnShellError !== true || options?.waitForAllReady === true || typeof error?.digest === "string") throw error;
362
+ } else if (options?.fallbackToErrorDocumentOnShellError !== true || isNavigationSignalError(error) || isBailoutToCSRError(error) || isDynamicServerError(error)) throw error;
359
363
  else {
360
364
  shellErrorRecovered = true;
361
- htmlStream = renderSsrErrorDocumentShell(bootstrapModuleUrl, options?.scriptNonce);
365
+ if (options?.isServerComponentError?.(error) !== true) ssrOnlyShellError = error;
366
+ htmlStream = renderSsrErrorDocumentShell(bootstrapModuleUrl, options?.scriptNonce, assetCrossOrigin);
362
367
  }
363
368
  }
364
369
  }
@@ -385,7 +390,7 @@ async function handleSsr(rscStream, navContext, fontData, options) {
385
390
  const getInsertedHTML = () => {
386
391
  const insertedHTML = renderInsertedHtml(renderServerInsertedHTML());
387
392
  const errorMetaHTML = errorMetaRenderer.flush();
388
- const initialDevServerErrorHTML = createInitialDevServerErrorScript(options?.initialDevServerError, options?.scriptNonce);
393
+ const initialDevServerErrorHTML = createInitialDevServerErrorScript(options?.initialDevServerError ?? ssrOnlyShellError, options?.scriptNonce);
389
394
  if (didInjectHeadHTML) return insertedHTML + errorMetaHTML + getRenderedSearchHTML();
390
395
  didInjectHeadHTML = true;
391
396
  const hidesQuery = searchParamsGate !== null && searchParamsGate.gate.decision !== "real";
@@ -107,11 +107,20 @@ export type SharedAppCacheIdentityProps = Exclude<AppWorkerResponseStageProps, {
107
107
  export declare function createSharedAppCacheIdentity(request: Request, props: SharedAppCacheIdentityProps): SharedAppCacheIdentity;
108
108
  /**
109
109
  * Build the query-free cache identity of a shared App page dispatch. The user
110
- * query leaves both the URL and `resolvedUrl`; the `.rsc` suffix, an RSC
111
- * request's validated `_rsc`, and the render mode stay because they select the
112
- * representation. HTML identities also drop the RSC selector headers.
110
+ * query leaves both the URL and `resolvedUrl`; an RSC request's validated
111
+ * `_rsc` and the render mode stay because they select the representation. HTML identities also drop the RSC selector headers.
113
112
  */
114
113
  export declare function createSharedAppPageCacheIdentity(request: Request, props: AppMatchedWorkerResponseStageProps): SharedAppCacheIdentity;
114
+ /**
115
+ * Build the query-free cache identity of a shared Route Handler or metadata
116
+ * route dispatch. Next.js keys a route handler by its pathname: the query and
117
+ * the RSC selectors pick no representation of a handler's response, and a
118
+ * handler that reads the request's URL or headers is dynamic, so admission
119
+ * refuses its response. Keeping them in the identity would only let a client
120
+ * store one entry per query or selector value. The method, path and route
121
+ * props stay; the selector headers and the props derived from them leave.
122
+ */
123
+ export declare function createSharedAppRouteCacheIdentity(request: Request, props: AppMatchedWorkerResponseStageProps | AppMetadataWorkerResponseStageProps): SharedAppCacheIdentity;
115
124
  /**
116
125
  * Build the cache identity of a shared hybrid Pages page dispatch: the
117
126
  * dispatched request and props under `origin`, so every host that names it
@@ -1,4 +1,5 @@
1
1
  import { VINEXT_RSC_VARY_HEADER } from "./app-rsc-vary.js";
2
+ import { APP_RSC_RENDER_MODE_NAVIGATION } from "./app-rsc-render-mode.js";
2
3
  import { cacheabilityManifestPageState, cacheabilityRequestIdentity, cacheabilityRoutePathname } from "./cacheability-manifest.js";
3
4
  import { keepOnlyValidatedRscCacheBustingSearchParam } from "./app-rsc-cache-busting.js";
4
5
  import { isTrustedPrerenderState } from "./prerender-route-params.js";
@@ -32,9 +33,8 @@ function createSharedAppCacheIdentity(request, props) {
32
33
  }
33
34
  /**
34
35
  * Build the query-free cache identity of a shared App page dispatch. The user
35
- * query leaves both the URL and `resolvedUrl`; the `.rsc` suffix, an RSC
36
- * request's validated `_rsc`, and the render mode stay because they select the
37
- * representation. HTML identities also drop the RSC selector headers.
36
+ * query leaves both the URL and `resolvedUrl`; an RSC request's validated
37
+ * `_rsc` and the render mode stay because they select the representation. HTML identities also drop the RSC selector headers.
38
38
  */
39
39
  function createSharedAppPageCacheIdentity(request, props) {
40
40
  const queryFree = withoutAppPageDispatchQuery(request.url, props);
@@ -49,6 +49,39 @@ function createSharedAppPageCacheIdentity(request, props) {
49
49
  };
50
50
  }
51
51
  /**
52
+ * Build the query-free cache identity of a shared Route Handler or metadata
53
+ * route dispatch. Next.js keys a route handler by its pathname: the query and
54
+ * the RSC selectors pick no representation of a handler's response, and a
55
+ * handler that reads the request's URL or headers is dynamic, so admission
56
+ * refuses its response. Keeping them in the identity would only let a client
57
+ * store one entry per query or selector value. The method, path and route
58
+ * props stay; the selector headers and the props derived from them leave.
59
+ */
60
+ function createSharedAppRouteCacheIdentity(request, props) {
61
+ const url = new URL(request.url);
62
+ url.search = "";
63
+ const headers = new Headers(request.headers);
64
+ for (const name of VINEXT_RSC_VARY_HEADER.split(",")) headers.delete(name.trim());
65
+ const searchIndex = props.resolvedUrl.indexOf("?");
66
+ return {
67
+ ...sharedAppCacheIdentity(url.toString(), {
68
+ headers,
69
+ method: request.method
70
+ }, {
71
+ ...props,
72
+ ...props.kind === "app-metadata" ? {} : {
73
+ interceptionContext: null,
74
+ interceptionId: null
75
+ },
76
+ isRscRequest: false,
77
+ mountedSlotsHeader: null,
78
+ renderMode: APP_RSC_RENDER_MODE_NAVIGATION,
79
+ resolvedUrl: searchIndex === -1 ? props.resolvedUrl : props.resolvedUrl.slice(0, searchIndex)
80
+ }),
81
+ queryFree: true
82
+ };
83
+ }
84
+ /**
52
85
  * Build the cache identity of a shared hybrid Pages page dispatch: the
53
86
  * dispatched request and props under `origin`, so every host that names it
54
87
  * shares one entry.
@@ -141,4 +174,4 @@ function isResponseStageCacheability(value) {
141
174
  return (cacheability.probeMode === null || cacheability.probeMode === "probe" || cacheability.probeMode === "identity") && (cacheability.policyHeaders === null || Array.isArray(cacheability.policyHeaders) && cacheability.policyHeaders.every((entry) => Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string" && typeof entry[1] === "string")) && (cacheability.representation === void 0 || cacheability.representation === "app-route" || cacheability.representation === "html" || cacheability.representation === "pages-data" || cacheability.representation === "rsc-full" || cacheability.representation === "rsc-loading-shell") && typeof cacheability.resolvedRoutePathname === "string" && cacheability.resolvedRoutePathname.startsWith("/");
142
175
  }
143
176
  //#endregion
144
- export { APP_METADATA_RESPONSE_STAGE_NO_MATCH_HEADER, APP_WORKER_RESPONSE_STAGE_PROTOCOL_VERSION, createSharedAppCacheIdentity, createSharedAppPageCacheIdentity, createSharedHybridPagesCacheIdentity, isAppWorkerResponseStageProps, isStaticCandidateAppPageDispatch, prepareSharedAppPageDispatch, withoutAppPageDispatchQuery };
177
+ export { APP_METADATA_RESPONSE_STAGE_NO_MATCH_HEADER, APP_WORKER_RESPONSE_STAGE_PROTOCOL_VERSION, createSharedAppCacheIdentity, createSharedAppPageCacheIdentity, createSharedAppRouteCacheIdentity, createSharedHybridPagesCacheIdentity, isAppWorkerResponseStageProps, isStaticCandidateAppPageDispatch, prepareSharedAppPageDispatch, withoutAppPageDispatchQuery };
@@ -3,6 +3,13 @@ type ParallelSlotBindingState = "active" | "default" | "unmatched";
3
3
  export declare function createNestedBfcacheSlotSegmentId(parentSlotId: string, level: number): string;
4
4
  export declare function isNestedBfcacheSlotSegmentId(id: string): boolean;
5
5
  export declare function isNestedBfcacheSlotSegmentIdFor(id: string, parentSlotId: string): boolean;
6
+ /**
7
+ * Activity key for a route segment whose directory has no layout of its own.
8
+ * Next.js keeps a layout-router Activity list at every segment level, implicit
9
+ * layouts included, keyed by the child segment. The tag keeps these keys apart
10
+ * from identity strings, whose kinds never use it, and from BFCache ids.
11
+ */
12
+ export declare function createBfcacheChildSegmentStateKey(segmentPathKey: string): string;
6
13
  export type BfcacheSegmentIdentity = string;
7
14
  export type BfcacheSegmentDescriptor = {
8
15
  kind: "page";
@@ -13,6 +13,15 @@ function isNestedBfcacheSlotSegmentId(id) {
13
13
  function isNestedBfcacheSlotSegmentIdFor(id, parentSlotId) {
14
14
  return isNestedBfcacheSlotSegmentId(id) && id.startsWith(createNestedBfcacheSlotParentPrefix(parentSlotId));
15
15
  }
16
+ /**
17
+ * Activity key for a route segment whose directory has no layout of its own.
18
+ * Next.js keeps a layout-router Activity list at every segment level, implicit
19
+ * layouts included, keyed by the child segment. The tag keeps these keys apart
20
+ * from identity strings, whose kinds never use it, and from BFCache ids.
21
+ */
22
+ function createBfcacheChildSegmentStateKey(segmentPathKey) {
23
+ return JSON.stringify(["segment", segmentPathKey]);
24
+ }
16
25
  function deriveBfcacheSegmentIdentity(descriptor) {
17
26
  switch (descriptor.kind) {
18
27
  case "page":
@@ -49,4 +58,4 @@ function deriveBfcacheSegmentIdentity(descriptor) {
49
58
  }
50
59
  }
51
60
  //#endregion
52
- export { createNestedBfcacheSlotSegmentId, deriveBfcacheSegmentIdentity, isNestedBfcacheSlotSegmentId, isNestedBfcacheSlotSegmentIdFor };
61
+ export { createBfcacheChildSegmentStateKey, createNestedBfcacheSlotSegmentId, deriveBfcacheSegmentIdentity, isNestedBfcacheSlotSegmentId, isNestedBfcacheSlotSegmentIdFor };
@@ -118,7 +118,7 @@ function cacheabilityRequestIdentity(request, trustedRepresentation) {
118
118
  representation: "pages-data",
119
119
  requestKey
120
120
  };
121
- if (!(request.headers.get("RSC") === "1" || url.pathname.endsWith(".rsc"))) return (request.headers.get("Accept")?.toLowerCase() ?? "").includes("text/html") ? {
121
+ if (!(request.headers.get("RSC") === "1")) return (request.headers.get("Accept")?.toLowerCase() ?? "").includes("text/html") ? {
122
122
  representation: "html",
123
123
  requestKey
124
124
  } : {
@@ -146,7 +146,6 @@ function normalizeCacheabilityRoutePathname(pathname) {
146
146
  }
147
147
  function cacheabilityRoutePathname(pathname, representation) {
148
148
  const normalized = new URL(pathname, "http://vinext.local").pathname;
149
- if (representation === "rsc-full" || representation === "rsc-loading-shell") return normalizeCacheabilityRoutePathname(normalized.endsWith(".rsc") ? normalized.slice(0, -4) : normalized);
150
149
  if (representation !== "pages-data") return normalizeCacheabilityRoutePathname(normalized);
151
150
  const marker = normalized.indexOf("/_next/data/");
152
151
  const restWithBuildId = marker === -1 ? "" : normalized.slice(marker + 12);
@@ -41,7 +41,8 @@ export declare function isCrossSiteNoCorsRequest(secFetchSite: string | null | u
41
41
  * Validate a dev server request from a Node.js IncomingMessage.
42
42
  *
43
43
  * Returns null if the request is allowed, or a reason string if it should be blocked.
44
- * This is used by the Pages Router connect middleware.
44
+ * This is used by the vinext:dev-origin-check connect middleware and the Pages
45
+ * Router handler.
45
46
  */
46
47
  export declare function validateDevRequest(headers: {
47
48
  origin?: string;
@@ -74,7 +74,8 @@ function isCrossSiteNoCorsRequest(secFetchSite, secFetchMode) {
74
74
  * Validate a dev server request from a Node.js IncomingMessage.
75
75
  *
76
76
  * Returns null if the request is allowed, or a reason string if it should be blocked.
77
- * This is used by the Pages Router connect middleware.
77
+ * This is used by the vinext:dev-origin-check connect middleware and the Pages
78
+ * Router handler.
78
79
  */
79
80
  function validateDevRequest(headers, allowedDevOrigins) {
80
81
  if (isCrossSiteNoCorsRequest(headers["sec-fetch-site"], headers["sec-fetch-mode"])) return `cross-site no-cors request blocked`;
@@ -9,6 +9,7 @@ import { matchRoute } from "../routing/pages-router.js";
9
9
  import { isDangerousScheme } from "../shims/url-safety.js";
10
10
  import { extractLocaleFromUrl as extractLocaleFromUrl$1, resolvePagesI18nRequest } from "./pages-i18n.js";
11
11
  import { importModule, reportRequestError } from "./instrumentation.js";
12
+ import { logRequestError } from "./request-error-log.js";
12
13
  import { NEVER_CACHE_CONTROL } from "./cache-control.js";
13
14
  import { buildMissIsrCacheControl } from "./isr-decision.js";
14
15
  import { isOnDemandRevalidateRequest } from "./revalidation-request.js";
@@ -918,7 +919,7 @@ function createSSRHandler(server, runner, routes, pagesDir, i18nConfig, fileMatc
918
919
  _renderEnd = now();
919
920
  if (typeof routerShim.setSSRContext === "function") routerShim.setSSRContext(null);
920
921
  } catch (e) {
921
- console.error(e);
922
+ logRequestError(e);
922
923
  await reportRequestError(e, {
923
924
  path: url,
924
925
  method: req.method ?? "GET",
@@ -143,6 +143,11 @@ export declare function negotiateImageFormat(acceptHeader: string | null): strin
143
143
  * Optimized images are immutable because the URL encodes the transform params.
144
144
  */
145
145
  export declare const IMAGE_CACHE_CONTROL = "public, max-age=31536000, immutable";
146
+ /**
147
+ * Next.js's Cache-Control for an image whose source can change at its URL
148
+ * (anything but a hashed build asset), at the default `images.minimumCacheTTL`.
149
+ */
150
+ export declare const MUTABLE_IMAGE_SOURCE_CACHE_CONTROL = "public, max-age=14400, must-revalidate";
146
151
  /**
147
152
  * Content-Security-Policy for image optimization responses.
148
153
  * Blocks script execution and framing to prevent XSS via SVG or other
@@ -229,6 +229,11 @@ function negotiateImageFormat(acceptHeader) {
229
229
  */
230
230
  const IMAGE_CACHE_CONTROL = "public, max-age=31536000, immutable";
231
231
  /**
232
+ * Next.js's Cache-Control for an image whose source can change at its URL
233
+ * (anything but a hashed build asset), at the default `images.minimumCacheTTL`.
234
+ */
235
+ const MUTABLE_IMAGE_SOURCE_CACHE_CONTROL = "public, max-age=14400, must-revalidate";
236
+ /**
232
237
  * Content-Security-Policy for image optimization responses.
233
238
  * Blocks script execution and framing to prevent XSS via SVG or other
234
239
  * active content that might be served through the image endpoint.
@@ -397,4 +402,4 @@ function handleConfiguredImageOptimization(request, fetchAsset, allowedWidths, i
397
402
  }, allowedWidths, imageConfig);
398
403
  }
399
404
  //#endregion
400
- export { DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES, IMAGE_CACHE_CONTROL, IMAGE_CONTENT_SECURITY_POLICY, IMAGE_OPTIMIZATION_PATH, IMAGE_RESPONSE_CACHE_STATE, INVALID_IMAGE_SOURCE_MESSAGE, VINEXT_IMAGE_OPTIMIZATION_PATH, getImageOptimizer, handleConfiguredImageOptimization, handleImageOptimization, invalidImageSourceResponse, isImageOptimizationPath, isImageSourceBodyStatus, isSafeImageContentType, markImageOptimizationSignal, negotiateImageFormat, parseImageParams, readImageOptimizationSignal, resolveDevImageRedirect, restoreImageOptimizationSignalFromTransport, serializeImageOptimizationSignalForTransport, setImageOptimizer };
405
+ export { DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES, IMAGE_CACHE_CONTROL, IMAGE_CONTENT_SECURITY_POLICY, IMAGE_OPTIMIZATION_PATH, IMAGE_RESPONSE_CACHE_STATE, INVALID_IMAGE_SOURCE_MESSAGE, MUTABLE_IMAGE_SOURCE_CACHE_CONTROL, VINEXT_IMAGE_OPTIMIZATION_PATH, getImageOptimizer, handleConfiguredImageOptimization, handleImageOptimization, invalidImageSourceResponse, isImageOptimizationPath, isImageSourceBodyStatus, isSafeImageContentType, markImageOptimizationSignal, negotiateImageFormat, parseImageParams, readImageOptimizationSignal, resolveDevImageRedirect, restoreImageOptimizationSignalFromTransport, serializeImageOptimizationSignalForTransport, setImageOptimizer };
@@ -3,6 +3,7 @@ import { normalizePathnameForRouteMatch } from "../routing/utils.js";
3
3
  import { getRequestExecutionContext } from "../shims/request-context.js";
4
4
  import { isInterceptionMatchedUrlPath } from "./normalize-path.js";
5
5
  import { reportRequestError } from "./instrumentation.js";
6
+ import { logRequestError } from "./request-error-log.js";
6
7
  import { getCdnCacheAdapter } from "../shims/cdn-cache.js";
7
8
  import { shareIsolatePromise } from "./isolate-shared-promise.js";
8
9
  import { normalizeMountedSlotsHeader } from "./app-mounted-slots-header.js";
@@ -144,7 +145,9 @@ function triggerBackgroundRegeneration(key, renderFn, errorContext) {
144
145
  if (!getCdnCacheAdapter().ownsBackgroundRevalidation) return;
145
146
  if (pendingRegenerations.has(key)) return;
146
147
  const promise = renderFn().catch(async (err) => {
147
- console.error(`[vinext] ISR background regeneration failed for ${key}:`, err);
148
+ const message = `[vinext] ISR background regeneration failed for ${key}:`;
149
+ if (errorContext) logRequestError(message, err);
150
+ else console.error(message, err);
148
151
  if (errorContext && (errorContext.shouldReport?.(err) ?? true)) await reportRequestError(err, {
149
152
  path: key,
150
153
  method: "GET",
@@ -14,10 +14,10 @@ import { isRenderDynamicLatched } from "../shims/internal/headers-state.js";
14
14
  import { getActiveDraftModeState, getHeadersContext, hasDraftModeCookieHeader, replaceHeadersContext } from "../shims/headers.js";
15
15
  import { _consumeRequestScopedCacheLife, _isOnDemandRevalidate } from "../shims/cache-request-state.js";
16
16
  import { ensureFetchPatch, getCollectedFetchTags, setCurrentFetchSoftTags } from "../shims/fetch-cache.js";
17
+ import { applyPrerenderCacheLifeHeader, applyPrerenderCacheTagsHeader } from "./prerender-cache-life-header.js";
17
18
  import { isValidMetadataImageId, manifestToJson, matchMetadataRoutePattern, robotsToText, sitemapToXml } from "./metadata-routes.js";
18
19
  import { buildAppRouteCacheValue, buildRouteHandlerCachedResponse } from "./app-route-handler-response.js";
19
20
  import { canonicalizeAppPageParams } from "./app-page-segment-state.js";
20
- import { applyPrerenderCacheLifeHeader, applyPrerenderCacheTagsHeader } from "./prerender-cache-life-header.js";
21
21
  import { buildPageCacheTags } from "./implicit-tags.js";
22
22
  import { createStaticGenerationHeadersContext } from "./app-static-generation.js";
23
23
  import { completeAppRouteHandlerResponse } from "./app-route-handler-execution.js";
@@ -50,17 +50,30 @@ function matchesMiddlewarePathname(pathname, matcher, i18nConfig, localeContext)
50
50
  }
51
51
  function matchMatcherPattern(pathname, pattern, i18nConfig, localeContext) {
52
52
  if (localeContext?.kind === "next-data") return matchNextDataPattern(pathname, pattern, i18nConfig, true);
53
- if (!i18nConfig) return matchPattern(pathname, pattern);
53
+ if (!i18nConfig) return matchRequestPattern(pathname, pattern);
54
54
  if (localeContext) {
55
55
  if (localeContext.kind === "internal") return false;
56
- return matchPattern(localeContext.kind === "literal" ? stripFirstPathSegment(pathname) : pathname, pattern);
56
+ return matchRequestPattern(localeContext.kind === "literal" ? stripFirstPathSegment(pathname) : pathname, pattern);
57
57
  }
58
- return matchPattern(stripLocalePrefix(pathname, i18nConfig) ?? pathname, pattern);
58
+ return matchRequestPattern(stripLocalePrefix(pathname, i18nConfig) ?? pathname, pattern);
59
59
  }
60
60
  function matchObjectMatcher(pathname, matcher, i18nConfig, localeContext) {
61
61
  if (localeContext?.kind === "next-data") return matchNextDataPattern(pathname, matcher.source, i18nConfig, matcher.locale !== false);
62
62
  if (matcher.locale !== false) return matchMatcherPattern(pathname, matcher.source, i18nConfig, localeContext);
63
- return matchPattern(localeContext?.kind === "defaulted" ? `/${localeContext.defaultLocale}${pathname === "/" ? "" : pathname}` : pathname, matcher.source);
63
+ return matchRequestPattern(localeContext?.kind === "defaulted" ? `/${localeContext.defaultLocale}${pathname === "/" ? "" : pathname}` : pathname, matcher.source);
64
+ }
65
+ const APP_ROUTE_TRANSPORT_SUFFIX = "\\.rsc|\\.segments/.+\\.segment\\.rsc";
66
+ const APP_ROUTE_TRANSPORT_SUFFIX_RE = /(?:\.rsc|\.segments\/.+\.segment\.rsc)$/;
67
+ /**
68
+ * Next.js compiles every matcher with optional App Router transport suffixes,
69
+ * so a matcher for `/about` runs middleware for `/about.rsc` too. Middleware
70
+ * still sees the path as requested.
71
+ * https://github.com/vercel/next.js/blob/v16.2.7/packages/next/src/build/analysis/get-page-static-info.ts
72
+ */
73
+ function matchRequestPattern(pathname, source) {
74
+ if (matchPattern(pathname, source)) return true;
75
+ if (!APP_ROUTE_TRANSPORT_SUFFIX_RE.test(pathname)) return false;
76
+ return matchPattern(pathname, source === "/" ? `/(/?index(?:${APP_ROUTE_TRANSPORT_SUFFIX}))?` : `${source}{(${APP_ROUTE_TRANSPORT_SUFFIX})}?`);
64
77
  }
65
78
  function matchNextDataPattern(pathname, source, i18nConfig, localized) {
66
79
  if (compiledPattern(source) === INVALID_MATCHER_PATTERN) return true;
@@ -69,8 +82,8 @@ function matchNextDataPattern(pathname, source, i18nConfig, localized) {
69
82
  /**
70
83
  * The source Next.js compiles for a matcher: an optional `_next/data/<buildId>`
71
84
  * prefix, an internal locale segment under i18n, and an optional data suffix.
72
- * Next.js also accepts App Router transport suffixes, which end in `.rsc` and
73
- * so can never match a `.json` data URL.
85
+ * Its App Router transport suffixes end in `.rsc`, so they can never match a
86
+ * `.json` data URL.
74
87
  * https://github.com/vercel/next.js/blob/v16.2.7/packages/next/src/build/analysis/get-page-static-info.ts
75
88
  */
76
89
  function nextDataMatcherSource(source, i18nConfig, localized) {
@@ -19,6 +19,7 @@ import { nextDataMiddlewareMatchPathname } from "./pages-data-route.js";
19
19
  import { reportRequestError } from "./instrumentation.js";
20
20
  import { isValidNavigationSignalError } from "../utils/navigation-signal.js";
21
21
  import { frameworkTracer } from "./tracer.js";
22
+ import { logRequestError } from "./request-error-log.js";
22
23
  //#region src/server/middleware-runtime.ts
23
24
  function reportProxyError(options, error) {
24
25
  const url = new URL(options.request.url);
@@ -252,8 +253,11 @@ async function executeMiddlewareModule(options) {
252
253
  } catch (e) {
253
254
  const isDevelopmentNavigationSignal = process.env.NODE_ENV !== "production" && isValidNavigationSignalError(e);
254
255
  if (isDevelopmentNavigationSignal && e instanceof Error) e.message = `Next.js navigation API is not allowed to be used in ${options.isProxy ? "Proxy" : "Middleware"}.`;
255
- console.error("[vinext] Middleware error:", e);
256
- if (!isDevelopmentNavigationSignal) await reportProxyError(options, e);
256
+ if (isDevelopmentNavigationSignal) console.error("[vinext] Middleware error:", e);
257
+ else {
258
+ logRequestError("[vinext] Middleware error:", e);
259
+ await reportProxyError(options, e);
260
+ }
257
261
  const waitUntilPromises = drainFetchEvent(fetchEvent);
258
262
  releaseMiddlewareRequestBody(nextRequest, waitUntilPromises);
259
263
  const message = options.includeErrorDetails ? "Middleware Error: " + (e instanceof Error ? e.message : String(e)) : "Internal Server Error";
@@ -77,14 +77,18 @@ export type VinextResponseStageDispatchOptions = {
77
77
  *
78
78
  * Under `responseStageCacheIdentity: "query-free"`, an App page identity
79
79
  * also drops the user query from the URL and `resolvedUrl`, keeping
80
- * framework representation selectors: the `.rsc` suffix, the render mode,
81
- * and, for RSC requests only, the one validated `_rsc` value. That is safe
80
+ * framework representation selectors: the render mode and, for RSC
81
+ * requests only, the one validated `_rsc` value. That is safe
82
82
  * only because completed-response admission refuses a cacheable App page
83
83
  * response unless its render proved it never read `searchParams`. The
84
84
  * dispatched request still carries the real query for the render itself. A
85
85
  * page identity keeps the query when a `next.config` public cache policy
86
86
  * applies (cached per full URL, as Next.js CDN caching is), and for
87
- * interception and mounted-slot dispatches.
87
+ * interception and mounted-slot dispatches. A Route Handler or metadata
88
+ * route identity keeps only the method, path (without `.rsc`) and route
89
+ * props, as Next.js keys a route handler by its pathname: the RSC selectors
90
+ * pick no representation of its response, and admission refuses a handler
91
+ * that read the request's URL or headers.
88
92
  */
89
93
  cacheIdentity?: {
90
94
  props: unknown;
@@ -7,7 +7,7 @@ export type PagesRequestQuery = Record<string, string | string[]>;
7
7
  export type PagesReqResRequest = Readable & {
8
8
  method: string;
9
9
  url: string;
10
- headers: Record<string, string>;
10
+ headers: Record<string, string | string[] | undefined>;
11
11
  query: PagesRequestQuery;
12
12
  body: unknown;
13
13
  cookies: Record<string, string>;
@@ -367,6 +367,8 @@ function isVinextStreamedApiResponse(response) {
367
367
  function createPagesReqRes(options) {
368
368
  const headersObj = {};
369
369
  for (const [key, value] of options.request.headers) headersObj[key.toLowerCase()] = value;
370
+ const setCookies = options.request.headers.getSetCookie();
371
+ if (setCookies.length > 1) headersObj["set-cookie"] = setCookies;
370
372
  const req = Object.assign(createRequestReadable(options.request), {
371
373
  method: options.request.method,
372
374
  url: options.url,
@@ -13,7 +13,7 @@ import { normalizeStaticPathname } from "../routing/route-pattern.js";
13
13
  import { buildCacheStateHeaders } from "./cache-headers.js";
14
14
  import { pagesPathCacheTag } from "./pages-cache-tag.js";
15
15
  import { isPageNotModified } from "./http-conditional.js";
16
- import { generatePagesETag, isPagesStreamingBot, rendersPagesNotModified } from "./pages-page-response.js";
16
+ import { applyPagesPrerenderRevalidateHeader, generatePagesETag, isPagesStreamingBot, rendersPagesNotModified } from "./pages-page-response.js";
17
17
  import { createPagesGetInitialPropsRouter, hasPagesGetInitialProps, isResponseSent, loadPagesGetInitialProps } from "./pages-get-initial-props.js";
18
18
  import { isSerializableProps } from "./pages-serializable-props.js";
19
19
  import { tracePagesData, tracePagesDocument } from "./pages-execution-tracing.js";
@@ -130,6 +130,7 @@ function applyPagesTerminalMissHeaders(response, revalidateSeconds, isrCachePath
130
130
  tags: [pagesPathCacheTag(isrCachePathname)]
131
131
  });
132
132
  for (const [name, value] of Object.entries(buildCacheStateHeaders("MISS"))) response.headers.set(name, value);
133
+ applyPagesPrerenderRevalidateHeader(response.headers, revalidateSeconds);
133
134
  return response;
134
135
  }
135
136
  function applyCachedPagesRepresentationHeaders(response, cacheState, entry, options) {