Repository navigation
fix(app-render): stop prerendered Link headers from accumulating on revalidation - #100960
Merged
unstubbable merged 1 commit intoNov 6, 2026
Merged
Conversation
muaaziqbalvip
added a commit
to muaaziqbalvip/next.js
that referenced
this pull request
Oct 30, 2026
- fix(app-render): stop prerendered Link headers from accumulating on revalidation. prerenderToStream now records only the headers it produces (React onHeaders output, redirect location) in metadata.headers instead of snapshotting res.getHeader(name), and no longer writes them to the response directly. Consumers already apply metadata.headers exactly once. Fixes vercel#100959. Upstream: vercel#100960. - perf(build): use nft's cached readlink when collecting build traces by removing the custom readlink override (identical error semantics: null on EINVAL/ENOENT/UNKNOWN, cached per path). Fixes vercel#100869. Upstream: vercel#99678. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Contributor
Tests PassedCommit: 52e185c |
β¦evalidation With `cacheComponents`, `prerenderToStream` wrote the headers React emits through `onHeaders` to the live response and then stored a snapshot of the response's whole header (`res.getHeader(name)`) in `metadata.headers`, which becomes the APP_PAGE cache entry's headers. That snapshot also captured values the render did not produce: a `Link` set by `headers()` in next.config.js, and, during a background revalidation, the `Link` values replayed from the previous cache entry. Since the cache entry's headers are appended to every response after the config headers, each revalidation added another copy of the config `Link`. On an on-demand render (cache MISS), the headers written to the response were appended a second time from `metadata.headers`, so both the config and the preload `Link` appeared twice. Record only the values the prerender produces in `metadata.headers` and leave applying them to the response to the caller, which already does so for both fresh renders and cache hits. A `Link` set by middleware or next.config.js is still merged with React's preload `Link`, because the cached value is appended to the response rather than replacing it.
unstubbable
force-pushed
the
fix/app-page-link-header-accumulation
branch
from
November 6, 2026 16:49
0086dc0 to
52e185c
Compare
unstubbable
approved these changes
Nov 6, 2026
unstubbable
added a commit
that referenced
this pull request
Nov 6, 2026
When a navigation reused cached data from a stale partial prerender, Next.js could render its Flight response while regenerating the prerender in the background. Both renders had access to the same HTTP response. A background redirect could change the foreground response's status before its headers committed and force a document navigation. The shared status also caused problems in the other direction: a prerender could observe the stale document response's 404 status and include `noindex` in otherwise successful regenerated content. This builds on the header isolation in PR #100960 and extracts the independent prerender output work from PR #100472. That draft explores a broader separation between App Router rendering and HTTP transport. We extract this part so the ownership change can be reviewed and landed independently of its action, cookie, and lifecycle refactors. The existing foreground/background scheduling remains intact, as it did in PR #100854. Prerendering now receives an explicit initial status and owns its status throughout the render. Normal prerenders start at 200, while error-page routes establish their intended status. The shared rendering context exposes only status instead of the whole HTTP response, and prerendering returns its status through result metadata. Dynamic rendering continues to use the live response. Delivery applies the existing HTML and RSC status rules to the selected result before invoking `onCacheEntry` or `onCacheEntryV2`. Those callbacks can write and finish the response, so applying status afterward is too late. They previously relied on prerendering having already changed the live response. App Page export now consumes the returned metadata directly instead of constructing request and response mocks to observe those mutations. Co-authored-by: Jan Kassens <kassens@vercel.com>
5 tasks
unstubbable
added a commit
that referenced
this pull request
Nov 8, 2026
With Cache Components enabled, `next start` served a generic fallback shell when a navigation to a prerendered path missed a custom cache. The incomplete RSC response crashed the client with React #412, as reported in #99861. Static paths that regenerated as partial were also served as static RSC, without the request data required by navigations and runtime prefetches. PR #101125 addresses the navigation miss and returns a 404 when a navigation still receives incomplete output. The router then reloads the document. This alternative builds on that work and its regression cases to keep navigation on the client and fix runtime prefetching too. Runtime prefetches must resolve the data required at their requested stage, such as cookies, while allowing valid later-stage holes. We use the RSC classification introduced in #101309 to choose the miss policy. On a miss, requests whose RSC was complete at build time wait for a coalesced prerender. Other classified RSC requests start a request-specific render while a coalesced background prerender populates the cache. Explicit blocking policies still take precedence. After a blocking prerender, the handler checks the actual result before serving it. It also checks cached entries and fallback results shared with document requests. If the result is partial, lacks usable RSC, or comes from a generic fallback shell, each caller renders its own RSC response. That render runs outside the shared response cache and retains the original router state and prefetch stage. It can reuse the Resume Data Cache from postponed state. The response isolation from #100960 and #101295 keeps prerender headers and status separate from the request-specific response. This aligns `next start` RSC miss handling with the per-output `onMiss` behavior planned in nextjs/adapter-vercel#114. We plan an HTML follow-up as part of the same parity work once its miss policy is agreed. A separate failure on Vercel affects static paths that regenerate as partial: RSC requests return `500 INTERNAL_INVALID_RESPONSE`. That failure predates this change and needs its own follow-up. Co-authored-by: Arnaud Coral <arnaud@datarooster.io>
unstubbable
added a commit
that referenced
this pull request
Nov 9, 2026
With Cache Components enabled, `next start` served a generic fallback shell when a navigation to a prerendered path missed a custom cache. The incomplete RSC response crashed the client with React #412, as reported in #99861. Static paths that regenerated as partial were also served as static RSC, without the request data required by navigations and runtime prefetches. PR #101125 addresses the navigation miss and returns a 404 when a navigation still receives incomplete output. The router then reloads the document. This alternative builds on that work and its regression cases to keep navigation on the client and fix runtime prefetching too. Runtime prefetches must resolve the data required at their requested stage, such as cookies, while allowing valid later-stage holes. We use the RSC classification introduced in #101309 to choose the miss policy. On a miss, requests whose RSC was complete at build time wait for a coalesced prerender. Other classified RSC requests start a request-specific render while a coalesced background prerender populates the cache. Explicit blocking policies still take precedence. After a blocking prerender, the handler checks the actual result before serving it. It also checks cached entries and fallback results shared with document requests. If the result is partial, lacks usable RSC, or comes from a generic fallback shell, each caller renders its own RSC response. That render runs outside the shared response cache and retains the original router state and prefetch stage. It can reuse the Resume Data Cache from postponed state. The response isolation from #100960 and #101295 keeps prerender headers and status separate from the request-specific response. This aligns `next start` RSC miss handling with the per-output `onMiss` behavior planned in nextjs/adapter-vercel#114. We plan an HTML follow-up as part of the same parity work once its miss policy is agreed. A separate failure on Vercel affects static paths that regenerate as partial: RSC requests return `500 INTERNAL_INVALID_RESPONSE`. That failure predates this change and needs its own follow-up. Co-authored-by: Arnaud Coral <arnaud@datarooster.io>
unstubbable
added a commit
that referenced
this pull request
Nov 9, 2026
With Cache Components enabled, `next start` served a generic fallback shell when a navigation to a prerendered path missed a custom cache. The incomplete RSC response crashed the client with React #412, as reported in #99861. Static paths that regenerated as partial were also served as static RSC, without the request data required by navigations and runtime prefetches. PR #101125 addresses the navigation miss and returns a 404 when a navigation still receives incomplete output. The router then reloads the document. This alternative builds on that work and its regression cases to keep navigation on the client and fix runtime prefetching too. Runtime prefetches must resolve the data required at their requested stage, such as cookies, while allowing valid later-stage holes. We use the RSC classification introduced in #101309 to choose the miss policy. On a miss, requests whose RSC was complete at build time wait for a coalesced prerender. Other classified RSC requests start a request-specific render while a coalesced background prerender populates the cache. Explicit blocking policies still take precedence. After a blocking prerender, the handler checks the actual result before serving it. It also checks cached entries and fallback results shared with document requests. If the result is partial, lacks usable RSC, or comes from a generic fallback shell, each caller renders its own RSC response. That render runs outside the shared response cache and retains the original router state and prefetch stage. It can reuse the Resume Data Cache from postponed state. The response isolation from #100960 and #101295 keeps prerender headers and status separate from the request-specific response. This aligns `next start` RSC miss handling with the per-output `onMiss` behavior planned in nextjs/adapter-vercel#114. We plan an HTML follow-up as part of the same parity work once its miss policy is agreed. A separate failure on Vercel affects static paths that regenerate as partial: RSC requests return `500 INTERNAL_INVALID_RESPONSE`. That failure predates this change and needs its own follow-up. Co-authored-by: Arnaud Coral <arnaud@datarooster.io>
unstubbable
added a commit
that referenced
this pull request
Nov 9, 2026
When a navigation reused cached data from a stale partial prerender, Next.js could render its Flight response while regenerating the prerender in the background. Both renders had access to the same HTTP response. A background redirect could change the foreground response's status before its headers committed and force a document navigation. The shared status also caused problems in the other direction: a prerender could observe the stale document response's 404 status and include `noindex` in otherwise successful regenerated content. This builds on the header isolation in PR #100960 and extracts the independent prerender output work from PR #100472. That draft explores a broader separation between App Router rendering and HTTP transport. We extract this part so the ownership change can be reviewed and landed independently of its action, cookie, and lifecycle refactors. The existing foreground/background scheduling remains intact, as it did in PR #100854. Prerendering now receives an explicit initial status and owns its status throughout the render. Normal prerenders start at 200, while error-page routes establish their intended status. The shared rendering context exposes only status instead of the whole HTTP response, and prerendering returns its status through result metadata. Dynamic rendering continues to use the live response. Delivery applies the existing HTML and RSC status rules to the selected result before invoking `onCacheEntry` or `onCacheEntryV2`. Those callbacks can write and finish the response, so applying status afterward is too late. They previously relied on prerendering having already changed the live response. App Page export now consumes the returned metadata directly instead of constructing request and response mocks to observe those mutations. Co-authored-by: @kassens Co-authored-by: Jan Kassens <kassens@vercel.com>
unstubbable
added a commit
that referenced
this pull request
Nov 9, 2026
With Cache Components enabled, `next start` served a generic fallback shell when a navigation to a prerendered path missed a custom cache. The incomplete RSC response crashed the client with React #412, as reported in #99861. Static paths that regenerated as partial were also served as static RSC, without the request data required by navigations and runtime prefetches. PR #101125 addresses the navigation miss and returns a 404 when a navigation still receives incomplete output. The router then reloads the document. This alternative builds on that work and its regression cases to keep navigation on the client and fix runtime prefetching too. Runtime prefetches must resolve the data required at their requested stage, such as cookies, while allowing valid later-stage holes. We use the RSC classification introduced in #101309 to choose the miss policy. On a miss, requests whose RSC was complete at build time wait for a coalesced prerender. Other classified RSC requests start a request-specific render while a coalesced background prerender populates the cache. Explicit blocking policies still take precedence. After a blocking prerender, the handler checks the actual result before serving it. It also checks cached entries and fallback results shared with document requests. If the result is partial, lacks usable RSC, or comes from a generic fallback shell, each caller renders its own RSC response. That render runs outside the shared response cache and retains the original router state and prefetch stage. It can reuse the Resume Data Cache from postponed state. The response isolation from #100960 and #101295 keeps prerender headers and status separate from the request-specific response. This aligns `next start` RSC miss handling with the per-output `onMiss` behavior planned in nextjs/adapter-vercel#114. We plan an HTML follow-up as part of the same parity work once its miss policy is agreed. A separate failure on Vercel affects static paths that regenerate as partial: RSC requests return `500 INTERNAL_INVALID_RESPONSE`. That failure predates this change and needs its own follow-up. Co-authored-by: Arnaud Coral <arnaud@datarooster.io>
unstubbable
added a commit
that referenced
this pull request
Nov 9, 2026
With Cache Components enabled, `next start` served a generic fallback shell when a navigation to a prerendered path missed a custom cache. The incomplete RSC response crashed the client with React #412, as reported in #99861. Static paths that regenerated as partial were also served as static RSC, without the request data required by navigations and runtime prefetches. PR #101125 addresses the navigation miss and returns a 404 when a navigation still receives incomplete output. The router then reloads the document. This alternative builds on that work and its regression cases to keep navigation on the client and fix runtime prefetching too. Runtime prefetches must resolve the data required at their requested stage, such as cookies, while allowing valid later-stage holes. We use the RSC classification introduced in #101309 to choose the miss policy. On a miss, requests whose RSC was complete at build time wait for a coalesced prerender. Other classified RSC requests start a request-specific render while a coalesced background prerender populates the cache. Explicit blocking policies still take precedence. After a blocking prerender, the handler checks the actual result before serving it. It also checks cached entries and fallback results shared with document requests. If the result is partial, lacks usable RSC, or comes from a generic fallback shell, each caller renders its own RSC response. That render runs outside the shared response cache and retains the original router state and prefetch stage. It can reuse the Resume Data Cache from postponed state. The response isolation from #100960 and #101295 keeps prerender headers and status separate from the request-specific response. This aligns `next start` RSC miss handling with the per-output `onMiss` behavior planned in nextjs/adapter-vercel#114. We plan an HTML follow-up as part of the same parity work once its miss policy is agreed. A separate failure on Vercel affects static paths that regenerate as partial: RSC requests return `500 INTERNAL_INVALID_RESPONSE`. That failure predates this change and needs its own follow-up. Co-authored-by: Arnaud Coral <arnaud@datarooster.io>
unstubbable
added a commit
that referenced
this pull request
Nov 10, 2026
With Cache Components enabled, `next start` served a generic fallback shell when a navigation to a prerendered path missed a custom cache. The incomplete RSC response crashed the client with React #412, as reported in #99861. Static paths that regenerated as partial were also served as static RSC, without the request data required by navigations and runtime prefetches. PR #101125 addresses the navigation miss and returns a 404 when a navigation still receives incomplete output. The router then reloads the document. This alternative builds on that work and its regression cases to keep navigation on the client and fix runtime prefetching too. Runtime prefetches must resolve the data required at their requested stage, such as cookies, while allowing valid later-stage holes. We use the RSC classification introduced in #101309 to choose the miss policy. On a miss, requests whose RSC was complete at build time wait for a coalesced prerender. Other classified RSC requests start a request-specific render while a coalesced background prerender populates the cache. Explicit blocking policies still take precedence. After a blocking prerender, the handler checks the actual result before serving it. It also checks cached entries and fallback results shared with document requests. If the result is partial, lacks usable RSC, or comes from a generic fallback shell, each caller renders its own RSC response. That render runs outside the shared response cache and retains the original router state and prefetch stage. It can reuse the Resume Data Cache from postponed state. The response isolation from #100960 and #101295 keeps prerender headers and status separate from the request-specific response. This aligns `next start` RSC miss handling with the per-output `onMiss` behavior planned in nextjs/adapter-vercel#114. We plan an HTML follow-up as part of the same parity work once its miss policy is agreed. A separate failure on Vercel affects static paths that regenerate as partial: RSC requests return `500 INTERNAL_INVALID_RESPONSE`. That failure predates this change and needs its own follow-up. Co-authored-by: Arnaud Coral <arnaud@datarooster.io>
unstubbable
added a commit
that referenced
this pull request
Nov 11, 2026
With Cache Components enabled, `next start` served a generic fallback shell when a navigation to a prerendered path missed a custom cache. The incomplete RSC response crashed the client with React #412, as reported in #99861. Static paths that regenerated as partial were also served as static RSC, without the request data required by navigations and runtime prefetches. PR #101125 addresses the navigation miss and returns a 404 when a navigation still receives incomplete output. The router then reloads the document. This alternative builds on that work and its regression cases to keep navigation on the client and handle runtime prefetches at their requested stage. The handler derives the RSC miss policy from the classification introduced in #101511 and extended in #101309. RSC outputs classified as `static` or `blocking` wait for a coalesced prerender when their entry is missing or hard-expired. Other eligible RSC requests render independently while a coalesced background prerender populates the cache. A concrete build-time entry takes precedence over its generic matcher. A built partial path therefore renders dynamically even when the matcher requires blocking for paths not generated at build time. Paths generated on demand retain their matcher's policy after expiration or eviction. After a blocking prerender, the handler checks the actual result before serving it. It also checks cached entries and fallback results shared with document requests. Partial results, missing usable RSC, and generic fallback shells require a separate RSC render for each caller. That render runs outside the shared response cache, preserves router state and prefetch stage, and can reuse the Resume Data Cache from postponed state. The response isolation from #100960 and #101295 keeps prerender headers and status separate from the request-specific response. Deployment parity assumes the proposed compute-based mapping in nextjs/adapter-vercel#114. That draft still derives `onMiss` from `response`, and shared metadata consumers also need to recognize `compute: 'dynamic'`. HTML miss handling in `next start` remains a follow-up. Co-authored-by: Arnaud Coral <arnaud@datarooster.io>
unstubbable
pushed a commit
that referenced
this pull request
Nov 12, 2026
PR #100960 already fixes the header accumulation reported in #94945. This change retains the Proxy rewrite and `next/font/local` regression coverage from PR #95558 without its superseded framework change. The test runs in production and deploy modes. It checks four alternate links and one font preload across three regenerations and uses a rendered timestamp to confirm that each regeneration completed.
unstubbable
added a commit
that referenced
this pull request
Nov 12, 2026
Adopts #95558. Closes #95558. Fixes #94945. Proxy can add alternate-language `Link` headers while `next/font/local` adds font preloads. PR #100960 already fixes the accumulation reported in #94945, so this PR retains the regression coverage without the superseded framework change. The test checks four alternate links and one font preload across three confirmed cache regenerations. It runs in production and deploy modes, reads headers through the standard API, and observes a rendered timestamp instead of relying on fixed delays. Co-authored-by: KashapovK <kashapov.konstantin.hr@gmail.com>
unstubbable
added a commit
that referenced
this pull request
Nov 13, 2026
With Cache Components enabled, `next start` serves a generic fallback shell to an RSC navigation when a path prerendered at build time is missing from the cache. The incomplete RSC response causes the React #412 crash reported in #99861. The original reproduction uses a custom cache handler whose store lacks the build-time entry, but the runtime fix applies regardless of the cache backend. A related problem occurs when a build-time static path regenerates as partial: RSC requests still follow the static serving path and lack the request-specific data needed by navigations and runtime prefetches. PR #101125 addresses the navigation miss and returns a 404 when a navigation still receives incomplete output. The router then reloads the document. This alternative builds on that work and its regression cases to keep navigation on the client and handle runtime prefetches at their requested stage. The handler uses the classification from #101511 and #101309 to choose how to handle missing or hard-expired prerenders. RSC outputs classified as `static` or `blocking` wait for a coalesced prerender. Other eligible requests render independently while a coalesced background prerender populates the cache. A stale entry that remains usable keeps the existing stale-while-revalidate behavior. A concrete build-time entry takes precedence over its generic matcher. A built partial path therefore renders dynamically even when the matcher requires blocking for paths not generated at build time. Paths generated on demand retain their matcher's policy after expiration or eviction. After a blocking prerender, the handler checks the actual result before serving it. It also checks cached entries and fallback results shared with document requests. Partial results, missing usable RSC, and generic fallback shells require a separate RSC render for each caller. That render runs outside the shared response cache, preserves router state and prefetch stage, and can reuse the Resume Data Cache. The response isolation from #100960 and #101295 keeps prerender headers and status separate from the request-specific response. These miss-scheduling changes apply to `next start`. On Vercel, Proxy manages cache-miss scheduling. The per-output policy in nextjs/adapter-vercel#162 provides the corresponding deployment configuration. HTML miss handling in `next start` remains a follow-up. Fixes #99861 Closes #101125 Co-authored-by: Arnaud Coral <arnaud@datarooster.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What?
With
cacheComponents: true, aLinkheader set innext.config.jsheaders()(or by middleware) is duplicated on ISR pages that also emit a React preloadLink(e.g. an<img fetchPriority="high">), and gains one more copy on every background revalidation:An on-demand render (cache
MISS) of a page not prerendered at build time also returns both the configLinkand the preloadLinktwice.Why?
prerenderToStreamhandles React'sonHeadersby appending each value to the live response and then storingres.getHeader(name)inmetadata.headers, which becomes the APP_PAGE cache entry'sheaders. That snapshot includes values the render did not produce:Linkfromheaders()in next.config.js, already on the response before rendering;Linkvalues the serving path replayed from the previous cache entry onto the same response.app-page-runtimeappends the cache entry's headers to every response after the config headers are set, so each regeneration stores, and later serves, one more copy. On aMISSthe render has already written the headers to the response, and the same values are appended again frommetadata.headers.How?
prerenderToStreamnow records only the values it produces (React'sonHeadersoutput and the redirectlocation) inmetadata.headers, and no longer writes them to the response. Every consumer of a prerender result already appliesmetadata.headersto the response (app-page-runtimefor fresh renders and cache hits, the export worker at build time), so the headers reach the client exactly once. ALinkfrom middleware or next.config.js is still merged with React's preloadLink(#73431), because the cached value is appended to the response, not set over it.renderToStream(the dynamic path) is unchanged. It writesonHeadersoutput to the response only and never stores it inmetadata.headers, so it has no cache entry to accumulate into.Added
test/production/app-dir/cache-components-link-header, which configures aLinkinheaders(), renders a'use cache'page with a 1srevalidateand a high-priority image, and checks the configLinkand the preloadLinkappear exactly once on every response across four revalidations, for a build-time prerendered page, a prerendered dynamic param, and an on-demand dynamic param. Without the change it fails on webpack (the configLinkcount climbs 1, 2, 3, 4, 5, and theMISSresponse has 2 of each). With the change it passes on both webpack and Turbopack.One behavioural difference: the segment-prefetch cache branch returns before the cached headers are replayed, so an RSC segment-prefetch response that happens to trigger a fresh render no longer carries the preload
Link. Such responses have no use for a preload.Standalone reproduction: https://github.com/linyiru/next-cache-components-link-header-repro
Fixes #100959