ISG Pages With Custom _App GetInitialProps Can Override Or Error On Cache-Control Headers
When using Incremental Static Generation (ISG) via `unstable_revalidate`, a custom `_app` component's `getInitialProps` may attempt to set response headers (e.g. Cache-Control) after the page response has already been sent. In production this triggers 'Cannot set headers after they are sent' errors or silently overrides the ISG page's cache headers, while development mode does not reproduce the issue.
Next.js's ISG on-demand regeneration path reuses the original incoming request/response objects when executing `_app.getInitialProps` during background revalidation. By this time, the server may have already sent the cached page response (headers and body), so any attempt to set additional headers on the same `ServerResponse` fails or overrides previously sent headers.
1. Create a Next.js project with a page that exports `getStaticProps` returning `{ props: {...}, unstable_revalidate: 1 }`.
2. Create a custom `_app.js` with `getInitialProps` that calls `res.setHeader('Cache-Control', 'public, max-age=3600')` on the `ctx.res` object.
3. Build and start the production server (`next build && next start`).
4. Request the ISG page once to generate the static page.
5. Wait longer than the `unstable_revalidate` interval and request the page again to trigger on-demand revalidation.
6. Observe in server logs an error similar to `Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client`, or notice that the Cache-Control header from `_app` replaced the ISG page's header.
Fixing Code Block
// This is a patch-package compatible modification to Next.js's ISG render path.
// It ensures that _app.getInitialProps is called with a detached response object
// that discards header writes, preventing conflicts with the already-sent real response.
// File: node_modules/next/dist/next-server/server/next-server.js
// Locate the function that handles on-demand ISG rendering (around `renderToHTMLWithComponents`)
// and apply the following change:
// original snippet (before):
// const appCtx = { Component, ctx: { req, res, pathname, query, asPath, AppTree } };
// const appProps = await loadGetInitialProps(appCtx);
// patched snippet (after):
const { ServerResponse } = require('http');
const detachedRes = new ServerResponse(req);
detachedRes.setHeader = () => {};
detachedRes.writeHead = () => {};
detachedRes.end = () => {};
const appCtx = { Component, ctx: { req, res: detachedRes, pathname, query, asPath, AppTree } };
const appProps = await loadGetInitialProps(appCtx);
The patch replaces the real `ServerResponse` object with a dummy response when invoking `_app.getInitialProps` during ISG revalidation. This prevents user-defined header writes from either failing due to already-sent headers or overriding the page-level cache headers that were already committed. The real response remains untouched, ensuring the client receives the correct cached page with its original Cache-Control.
Edge Case Audit
This hotfix only addresses the ISG revalidation path. It may interfere with `_app.getInitialProps` logic that legitimately needs to inspect the response (e.g. reading headers, checking status code) during revalidation. Some apps rely on the response object for conditional logic; detaching it could cause unexpected behavior. Apply carefully and ensure thorough testing with your specific `_app` logic. Rollback: remove the patch and restart the server. Better long-term solution is to upgrade to a fixed Next.js version once available.