-
Notifications
You must be signed in to change notification settings - Fork 262
fix: replay render-time response headers for cached App Router responses #488
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
86aedca
7eff581
18f9483
d33fdb2
a2cddae
36f6ce4
17c5808
03ac94d
09b70a5
ca4e4c6
3979d44
c5d475e
b8ebae8
27287ab
4f9477e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1986,6 +1986,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { | |
| "vinext/i18n-context": path.join(shimsDir, "i18n-context"), | ||
| "vinext/instrumentation": path.resolve(__dirname, "server", "instrumentation"), | ||
| "vinext/html": path.resolve(__dirname, "server", "html"), | ||
| "vinext/server/app-router-entry": path.resolve(__dirname, "server", "app-router-entry"), | ||
| }).flatMap(([k, v]) => | ||
| k.startsWith("next/") | ||
| ? [ | ||
|
|
@@ -3267,13 +3268,18 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { | |
| if (hasAppDir) { | ||
| const mwCtxEntries: [string, string][] = []; | ||
| if (result.responseHeaders) { | ||
| const setCookies = result.responseHeaders.getSetCookie(); | ||
| for (const [key, value] of result.responseHeaders) { | ||
| // Exclude control headers that runMiddleware already | ||
| // consumed — matches the RSC entry's inline filtering. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good fix. The dev server middleware context forwarding now correctly uses |
||
| if (key === "set-cookie") continue; | ||
| if (key !== "x-middleware-next" && key !== "x-middleware-rewrite") { | ||
| mwCtxEntries.push([key, value]); | ||
| } | ||
| } | ||
| for (const cookie of setCookies) { | ||
| mwCtxEntries.push(["set-cookie", cookie]); | ||
| } | ||
| } | ||
| req.headers["x-vinext-mw-ctx"] = JSON.stringify({ | ||
| h: mwCtxEntries, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,7 @@ type AppPageCacheSetter = ( | |||||||||||||||||||||||||
| type AppPageBackgroundRegenerator = (key: string, renderFn: () => Promise<void>) => void; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export interface AppPageCacheRenderResult { | ||||||||||||||||||||||||||
| headers?: Record<string, string | string[]>; | ||||||||||||||||||||||||||
| html: string; | ||||||||||||||||||||||||||
| rscData: ArrayBuffer; | ||||||||||||||||||||||||||
| tags: string[]; | ||||||||||||||||||||||||||
|
|
@@ -40,7 +41,10 @@ export interface ReadAppPageCacheResponseOptions { | |||||||||||||||||||||||||
| export interface FinalizeAppPageHtmlCacheResponseOptions { | ||||||||||||||||||||||||||
| capturedRscDataPromise: Promise<ArrayBuffer> | null; | ||||||||||||||||||||||||||
| cleanPathname: string; | ||||||||||||||||||||||||||
| consumeDynamicUsage: () => boolean; | ||||||||||||||||||||||||||
| consumeRenderResponseHeaders: () => Record<string, string | string[]> | undefined; | ||||||||||||||||||||||||||
| getPageTags: () => string[]; | ||||||||||||||||||||||||||
| initialRenderHeaders?: Record<string, string | string[]>; | ||||||||||||||||||||||||||
| isrDebug?: AppPageDebugLogger; | ||||||||||||||||||||||||||
| isrHtmlKey: (pathname: string) => string; | ||||||||||||||||||||||||||
| isrRscKey: (pathname: string) => string; | ||||||||||||||||||||||||||
|
|
@@ -53,8 +57,10 @@ export interface ScheduleAppPageRscCacheWriteOptions { | |||||||||||||||||||||||||
| capturedRscDataPromise: Promise<ArrayBuffer> | null; | ||||||||||||||||||||||||||
| cleanPathname: string; | ||||||||||||||||||||||||||
| consumeDynamicUsage: () => boolean; | ||||||||||||||||||||||||||
| consumeRenderResponseHeaders: () => Record<string, string | string[]> | undefined; | ||||||||||||||||||||||||||
| dynamicUsedDuringBuild: boolean; | ||||||||||||||||||||||||||
| getPageTags: () => string[]; | ||||||||||||||||||||||||||
| initialRenderHeaders?: Record<string, string | string[]>; | ||||||||||||||||||||||||||
| isrDebug?: AppPageDebugLogger; | ||||||||||||||||||||||||||
| isrRscKey: (pathname: string) => string; | ||||||||||||||||||||||||||
| isrSet: AppPageCacheSetter; | ||||||||||||||||||||||||||
|
|
@@ -77,6 +83,70 @@ function getCachedAppPageValue(entry: ISRCacheEntry | null): CachedAppPageValue | |||||||||||||||||||||||||
| return entry?.value.value && entry.value.value.kind === "APP_PAGE" ? entry.value.value : null; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| function isAppendOnlyResponseHeader(lowerKey: string): boolean { | ||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||
| lowerKey === "set-cookie" || | ||||||||||||||||||||||||||
| lowerKey === "vary" || | ||||||||||||||||||||||||||
| lowerKey === "www-authenticate" || | ||||||||||||||||||||||||||
| lowerKey === "proxy-authenticate" | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| function mergeResponseHeaderValues( | ||||||||||||||||||||||||||
| targetHeaders: Headers, | ||||||||||||||||||||||||||
| key: string, | ||||||||||||||||||||||||||
| value: string | string[], | ||||||||||||||||||||||||||
| mode: "fallback" | "override", | ||||||||||||||||||||||||||
| ): void { | ||||||||||||||||||||||||||
| const lowerKey = key.toLowerCase(); | ||||||||||||||||||||||||||
| const values = Array.isArray(value) ? value : [value]; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (isAppendOnlyResponseHeader(lowerKey)) { | ||||||||||||||||||||||||||
| for (const item of values) { | ||||||||||||||||||||||||||
| targetHeaders.append(key, item); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (mode === "fallback" && targetHeaders.has(key)) { | ||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| targetHeaders.delete(key); | ||||||||||||||||||||||||||
| if (values.length === 1) { | ||||||||||||||||||||||||||
| targetHeaders.set(key, values[0]!); | ||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| for (const item of values) { | ||||||||||||||||||||||||||
| targetHeaders.append(key, item); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| function mergeResponseHeaders( | ||||||||||||||||||||||||||
| targetHeaders: Headers, | ||||||||||||||||||||||||||
| sourceHeaders: Record<string, string | string[]> | null | undefined, | ||||||||||||||||||||||||||
| mode: "fallback" | "override", | ||||||||||||||||||||||||||
| ): void { | ||||||||||||||||||||||||||
| if (!sourceHeaders) { | ||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| for (const [key, value] of Object.entries(sourceHeaders)) { | ||||||||||||||||||||||||||
| mergeResponseHeaderValues(targetHeaders, key, value, mode); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| function headersWithCachedHeaders( | ||||||||||||||||||||||||||
| baseHeaders: HeadersInit, | ||||||||||||||||||||||||||
| cachedHeaders: Record<string, string | string[]> | undefined, | ||||||||||||||||||||||||||
| ): Headers { | ||||||||||||||||||||||||||
| const headers = new Headers(); | ||||||||||||||||||||||||||
| mergeResponseHeaders(headers, cachedHeaders, "fallback"); | ||||||||||||||||||||||||||
| mergeResponseHeaders(headers, Object.fromEntries(new Headers(baseHeaders)), "override"); | ||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Fragile
Right now this is safe because The other two copies of this helper (
Suggested change
|
||||||||||||||||||||||||||
| return headers; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export function buildAppPageCachedResponse( | ||||||||||||||||||||||||||
| cachedValue: CachedAppPageValue, | ||||||||||||||||||||||||||
| options: BuildAppPageCachedResponseOptions, | ||||||||||||||||||||||||||
|
|
@@ -97,10 +167,13 @@ export function buildAppPageCachedResponse( | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return new Response(cachedValue.rscData, { | ||||||||||||||||||||||||||
| status, | ||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||
| "Content-Type": "text/x-component; charset=utf-8", | ||||||||||||||||||||||||||
| ...headers, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| headers: headersWithCachedHeaders( | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| "Content-Type": "text/x-component; charset=utf-8", | ||||||||||||||||||||||||||
| ...headers, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| cachedValue.headers, | ||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -110,10 +183,13 @@ export function buildAppPageCachedResponse( | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return new Response(cachedValue.html, { | ||||||||||||||||||||||||||
| status, | ||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||
| "Content-Type": "text/html; charset=utf-8", | ||||||||||||||||||||||||||
| ...headers, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| headers: headersWithCachedHeaders( | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| "Content-Type": "text/html; charset=utf-8", | ||||||||||||||||||||||||||
| ...headers, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| cachedValue.headers, | ||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -157,13 +233,13 @@ export async function readAppPageCacheResponse( | |||||||||||||||||||||||||
| await Promise.all([ | ||||||||||||||||||||||||||
| options.isrSet( | ||||||||||||||||||||||||||
| options.isrHtmlKey(options.cleanPathname), | ||||||||||||||||||||||||||
| buildAppPageCacheValue(revalidatedPage.html, undefined, 200), | ||||||||||||||||||||||||||
| buildAppPageCacheValue(revalidatedPage.html, undefined, revalidatedPage.headers, 200), | ||||||||||||||||||||||||||
| options.revalidateSeconds, | ||||||||||||||||||||||||||
| revalidatedPage.tags, | ||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||
| options.isrSet( | ||||||||||||||||||||||||||
| options.isrRscKey(options.cleanPathname), | ||||||||||||||||||||||||||
| buildAppPageCacheValue("", revalidatedPage.rscData, 200), | ||||||||||||||||||||||||||
| buildAppPageCacheValue("", revalidatedPage.rscData, revalidatedPage.headers, 200), | ||||||||||||||||||||||||||
| options.revalidateSeconds, | ||||||||||||||||||||||||||
| revalidatedPage.tags, | ||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||
|
|
@@ -225,11 +301,19 @@ export function finalizeAppPageHtmlCacheResponse( | |||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| chunks.push(decoder.decode()); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const renderHeadersForCache = | ||||||||||||||||||||||||||
| options.consumeRenderResponseHeaders() ?? options.initialRenderHeaders; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (options.consumeDynamicUsage()) { | ||||||||||||||||||||||||||
| options.isrDebug?.("skip HTML cache write after late dynamic usage", options.cleanPathname); | ||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const pageTags = options.getPageTags(); | ||||||||||||||||||||||||||
| const writes = [ | ||||||||||||||||||||||||||
| options.isrSet( | ||||||||||||||||||||||||||
| htmlKey, | ||||||||||||||||||||||||||
| buildAppPageCacheValue(chunks.join(""), undefined, 200), | ||||||||||||||||||||||||||
| buildAppPageCacheValue(chunks.join(""), undefined, renderHeadersForCache, 200), | ||||||||||||||||||||||||||
| options.revalidateSeconds, | ||||||||||||||||||||||||||
| pageTags, | ||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||
|
|
@@ -240,7 +324,7 @@ export function finalizeAppPageHtmlCacheResponse( | |||||||||||||||||||||||||
| options.capturedRscDataPromise.then((rscData) => | ||||||||||||||||||||||||||
| options.isrSet( | ||||||||||||||||||||||||||
| rscKey, | ||||||||||||||||||||||||||
| buildAppPageCacheValue("", rscData, 200), | ||||||||||||||||||||||||||
| buildAppPageCacheValue("", rscData, renderHeadersForCache, 200), | ||||||||||||||||||||||||||
| options.revalidateSeconds, | ||||||||||||||||||||||||||
| pageTags, | ||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||
|
|
@@ -252,6 +336,8 @@ export function finalizeAppPageHtmlCacheResponse( | |||||||||||||||||||||||||
| options.isrDebug?.("HTML cache written", htmlKey); | ||||||||||||||||||||||||||
| } catch (cacheError) { | ||||||||||||||||||||||||||
| console.error("[vinext] ISR cache write error:", cacheError); | ||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||
| options.consumeRenderResponseHeaders(); | ||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: double-consume could use a comment On the happy path,
Suggested change
|
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| })(); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -276,6 +362,8 @@ export function scheduleAppPageRscCacheWrite( | |||||||||||||||||||||||||
| const cachePromise = (async () => { | ||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||
| const rscData = await capturedRscDataPromise; | ||||||||||||||||||||||||||
| const renderHeadersForCache = | ||||||||||||||||||||||||||
| options.consumeRenderResponseHeaders() ?? options.initialRenderHeaders; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Two-phase dynamic detection: | ||||||||||||||||||||||||||
| // 1. dynamicUsedDuringBuild catches searchParams-driven opt-in before the | ||||||||||||||||||||||||||
|
|
@@ -289,7 +377,7 @@ export function scheduleAppPageRscCacheWrite( | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| await options.isrSet( | ||||||||||||||||||||||||||
| rscKey, | ||||||||||||||||||||||||||
| buildAppPageCacheValue("", rscData, 200), | ||||||||||||||||||||||||||
| buildAppPageCacheValue("", rscData, renderHeadersForCache, 200), | ||||||||||||||||||||||||||
| options.revalidateSeconds, | ||||||||||||||||||||||||||
| options.getPageTags(), | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,9 +47,12 @@ interface AppPageRequestCacheLife { | |
| } | ||
|
|
||
| export interface RenderAppPageLifecycleOptions { | ||
| buildDynamicUsage: boolean; | ||
| buildRenderHeaders?: Record<string, string | string[]>; | ||
| cleanPathname: string; | ||
| clearRequestContext: () => void; | ||
| consumeDynamicUsage: () => boolean; | ||
| consumeRenderResponseHeaders: () => Record<string, string | string[]> | undefined; | ||
| createRscOnErrorHandler: (pathname: string, routePath: string) => AppPageBoundaryOnError; | ||
| getFontLinks: () => string[]; | ||
| getFontPreloads: () => AppPageFontPreload[]; | ||
|
|
@@ -73,6 +76,8 @@ export interface RenderAppPageLifecycleOptions { | |
| loadSsrHandler: () => Promise<AppPageSsrHandler>; | ||
| middlewareContext: AppPageMiddlewareContext; | ||
| params: Record<string, unknown>; | ||
| peekDynamicUsage: () => boolean; | ||
| peekRenderResponseHeaders: () => Record<string, string | string[]> | undefined; | ||
| probeLayoutAt: (layoutIndex: number) => unknown; | ||
| probePage: () => unknown; | ||
| revalidateSeconds: number | null; | ||
|
|
@@ -86,6 +91,10 @@ export interface RenderAppPageLifecycleOptions { | |
| element: ReactNode, | ||
| options: { onError: AppPageBoundaryOnError }, | ||
| ) => ReadableStream<Uint8Array>; | ||
| restoreDynamicUsage: (dynamicUsed: boolean) => void; | ||
| restoreRenderResponseHeaders: ( | ||
| renderHeaders: Record<string, string | string[]> | undefined, | ||
| ) => void; | ||
| routeHasLocalBoundary: boolean; | ||
| routePattern: string; | ||
| runWithSuppressedHookWarning<T>(probe: () => Promise<T>): Promise<T>; | ||
|
|
@@ -139,6 +148,9 @@ export async function renderAppPageLifecycle( | |
| return preRenderResponse; | ||
| } | ||
|
|
||
| options.restoreRenderResponseHeaders(options.buildRenderHeaders); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good pattern. The probe phase may trigger |
||
| options.restoreDynamicUsage(options.buildDynamicUsage); | ||
|
|
||
| const compileEnd = options.isProduction ? undefined : performance.now(); | ||
| const baseOnError = options.createRscOnErrorHandler(options.cleanPathname, options.routePattern); | ||
| const rscErrorTracker = createAppPageRscErrorTracker(baseOnError); | ||
|
|
@@ -160,8 +172,12 @@ export async function renderAppPageLifecycle( | |
|
|
||
| if (options.isRscRequest) { | ||
| const dynamicUsedDuringBuild = options.consumeDynamicUsage(); | ||
| const lateDynamicUsage = isrRscDataPromise ? options.peekDynamicUsage() : false; | ||
| const rscResponseRenderHeaders = isrRscDataPromise | ||
| ? options.peekRenderResponseHeaders() | ||
| : options.consumeRenderResponseHeaders(); | ||
| const rscResponsePolicy = resolveAppPageRscResponsePolicy({ | ||
| dynamicUsedDuringBuild, | ||
| dynamicUsedDuringBuild: dynamicUsedDuringBuild || lateDynamicUsage, | ||
| isDynamicError: options.isDynamicError, | ||
| isForceDynamic: options.isForceDynamic, | ||
| isForceStatic: options.isForceStatic, | ||
|
|
@@ -172,6 +188,7 @@ export async function renderAppPageLifecycle( | |
| middlewareContext: options.middlewareContext, | ||
| params: options.params, | ||
| policy: rscResponsePolicy, | ||
| renderHeaders: rscResponseRenderHeaders, | ||
| timing: buildResponseTiming({ | ||
| compileEnd, | ||
| handlerStart: options.handlerStart, | ||
|
|
@@ -184,10 +201,12 @@ export async function renderAppPageLifecycle( | |
| capturedRscDataPromise: options.isProduction ? isrRscDataPromise : null, | ||
| cleanPathname: options.cleanPathname, | ||
| consumeDynamicUsage: options.consumeDynamicUsage, | ||
| dynamicUsedDuringBuild, | ||
| consumeRenderResponseHeaders: options.consumeRenderResponseHeaders, | ||
| dynamicUsedDuringBuild: dynamicUsedDuringBuild || lateDynamicUsage, | ||
| getPageTags() { | ||
| return options.getPageTags(); | ||
| }, | ||
| initialRenderHeaders: rscResponseRenderHeaders, | ||
| isrDebug: options.isrDebug, | ||
| isrRscKey: options.isrRscKey, | ||
| isrSet: options.isrSet, | ||
|
|
@@ -253,10 +272,15 @@ export async function renderAppPageLifecycle( | |
| } | ||
| } | ||
|
|
||
| const renderResponseHeaders = isrRscDataPromise | ||
| ? options.peekRenderResponseHeaders() | ||
| : options.consumeRenderResponseHeaders(); | ||
| options.clearRequestContext(); | ||
| const draftCookie = options.getDraftModeCookieHeader(); | ||
|
|
||
| const dynamicUsedDuringRender = options.consumeDynamicUsage(); | ||
| const dynamicUsedDuringRender = isrRscDataPromise | ||
| ? options.peekDynamicUsage() | ||
| : options.consumeDynamicUsage(); | ||
| const requestCacheLife = options.getRequestCacheLife(); | ||
| if (requestCacheLife?.revalidate !== undefined && revalidateSeconds === null) { | ||
| revalidateSeconds = requestCacheLife.revalidate; | ||
|
|
@@ -284,14 +308,18 @@ export async function renderAppPageLifecycle( | |
| fontLinkHeader, | ||
| middlewareContext: options.middlewareContext, | ||
| policy: htmlResponsePolicy, | ||
| renderHeaders: renderResponseHeaders, | ||
| timing: htmlResponseTiming, | ||
| }); | ||
| return finalizeAppPageHtmlCacheResponse(isrResponse, { | ||
| capturedRscDataPromise: isrRscDataPromise, | ||
| cleanPathname: options.cleanPathname, | ||
| consumeDynamicUsage: options.consumeDynamicUsage, | ||
| consumeRenderResponseHeaders: options.consumeRenderResponseHeaders, | ||
| getPageTags() { | ||
| return options.getPageTags(); | ||
| }, | ||
| initialRenderHeaders: renderResponseHeaders, | ||
| isrDebug: options.isrDebug, | ||
| isrHtmlKey: options.isrHtmlKey, | ||
| isrRscKey: options.isrRscKey, | ||
|
|
@@ -308,6 +336,7 @@ export async function renderAppPageLifecycle( | |
| fontLinkHeader, | ||
| middlewareContext: options.middlewareContext, | ||
| policy: htmlResponsePolicy, | ||
| renderHeaders: renderResponseHeaders, | ||
| timing: htmlResponseTiming, | ||
| }); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good fix. The dev server middleware context forwarding now correctly uses
getSetCookie()to preserve individual Set-Cookie values, matching the pattern in__copyResponseHeadersand__mergeResponseHeaders.