next.js
0755a381 - fix(app-render): stop prerendered Link headers from accumulating on revalidation (#100960)

Commit
1 day ago
fix(app-render): stop prerendered Link headers from accumulating on revalidation (#100960) ### What? With `cacheComponents: true`, a `Link` header set in `next.config.js` `headers()` (or by middleware) is duplicated on ISR pages that also emit a React preload `Link` (e.g. an `<img fetchPriority="high">`), and gains one more copy on every background revalidation: ``` req 3 Link: </llms.txt>; rel="llms-txt" Link: </llms.txt>; rel="llms-txt",</llms.txt>; rel="llms-txt",</hero.png>; rel=preload; as="image"; fetchpriority="high" req 4 Link: </llms.txt>; rel="llms-txt" Link: </llms.txt>; rel="llms-txt",</llms.txt>; rel="llms-txt",</llms.txt>; rel="llms-txt",</hero.png>; rel=preload; ... ... ``` An on-demand render (cache `MISS`) of a page not prerendered at build time also returns both the config `Link` and the preload `Link` twice. ### Why? `prerenderToStream` handles React's `onHeaders` by appending each value to the live response and then storing `res.getHeader(name)` in `metadata.headers`, which becomes the APP_PAGE cache entry's `headers`. That snapshot includes values the render did not produce: - the `Link` from `headers()` in next.config.js, already on the response before rendering; - during a background revalidation, the `Link` values the serving path replayed from the previous cache entry onto the same response. `app-page-runtime` appends 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 a `MISS` the render has already written the headers to the response, and the same values are appended again from `metadata.headers`. ### How? `prerenderToStream` now records only the values it produces (React's `onHeaders` output and the redirect `location`) in `metadata.headers`, and no longer writes them to the response. Every consumer of a prerender result already applies `metadata.headers` to the response (`app-page-runtime` for fresh renders and cache hits, the export worker at build time), so the headers reach the client exactly once. A `Link` from middleware or next.config.js is still merged with React's preload `Link` (#73431), because the cached value is appended to the response, not set over it. `renderToStream` (the dynamic path) is unchanged. It writes `onHeaders` output to the response only and never stores it in `metadata.headers`, so it has no cache entry to accumulate into. Added `test/production/app-dir/cache-components-link-header`, which configures a `Link` in `headers()`, renders a `'use cache'` page with a 1s `revalidate` and a high-priority image, and checks the config `Link` and the preload `Link` appear 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 config `Link` count climbs 1, 2, 3, 4, 5, and the `MISS` response 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 <!-- NEXT_JS_LLM -->
Author
Parents
Loading