diff --git a/errors/gssp-no-mutating-res.md b/errors/gssp-no-mutating-res.md index fa5af6313cdfb99..d40db77d6ecb7dc 100644 --- a/errors/gssp-no-mutating-res.md +++ b/errors/gssp-no-mutating-res.md @@ -12,6 +12,8 @@ For this reason, accessing the object after this time is disallowed. You can fix this error by moving any access of the `res` object into `getServerSideProps()` itself or any functions that run before `getServerSideProps()` returns. +If you’re using a custom server and running into this problem due to session middleware like `next-session` or `express-session`, try installing the middleware in the server instead of `getServerSideProps()`. + ### Useful Links - [Data Fetching Docs](https://nextjs.org/docs/basic-features/data-fetching) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 74907e1a173c7ee..740d1104493674a 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -745,14 +745,20 @@ export async function renderToHTML( let canAccessRes = true let resOrProxy = res + let deferredContent = false if (process.env.NODE_ENV !== 'production') { resOrProxy = new Proxy(res, { get: function (obj, prop, receiver) { if (!canAccessRes) { - throw new Error( + const message = `You should not access 'res' after getServerSideProps resolves.` + - `\nRead more: https://nextjs.org/docs/messages/gssp-no-mutating-res` - ) + `\nRead more: https://nextjs.org/docs/messages/gssp-no-mutating-res` + + if (deferredContent) { + throw new Error(message) + } else { + warn(message) + } } return Reflect.get(obj, prop, receiver) }, @@ -792,6 +798,10 @@ export async function renderToHTML( throw new Error(GSSP_NO_RETURNED_VALUE) } + if ((data as any).props instanceof Promise) { + deferredContent = true + } + const invalidKeys = Object.keys(data).filter( (key) => key !== 'props' && key !== 'redirect' && key !== 'notFound' ) @@ -834,7 +844,7 @@ export async function renderToHTML( ;(renderOpts as any).isRedirect = true } - if ((data as any).props instanceof Promise) { + if (deferredContent) { ;(data as any).props = await (data as any).props } diff --git a/test/integration/getserversideprops/pages/promise/mutate-res-no-streaming.js b/test/integration/getserversideprops/pages/promise/mutate-res-no-streaming.js new file mode 100644 index 000000000000000..fdc1a9b498dbdd5 --- /dev/null +++ b/test/integration/getserversideprops/pages/promise/mutate-res-no-streaming.js @@ -0,0 +1,20 @@ +let mutatedResNoStreaming + +export async function getServerSideProps(context) { + mutatedResNoStreaming = context.res + return { + props: { + text: 'res', + }, + } +} + +export default ({ text }) => { + mutatedResNoStreaming.setHeader('test-header', 'this is a test header') + + return ( + <> +
hello {text}
+ + ) +} diff --git a/test/integration/getserversideprops/test/index.test.js b/test/integration/getserversideprops/test/index.test.js index 5006b54b0502de8..99bfa6c8484cce5 100644 --- a/test/integration/getserversideprops/test/index.test.js +++ b/test/integration/getserversideprops/test/index.test.js @@ -147,6 +147,14 @@ const expectedManifestRoutes = () => [ ), page: '/promise/mutate-res', }, + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/promise\\/mutate-res-no-streaming.json$` + ), + page: '/promise/mutate-res-no-streaming', + }, { dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( @@ -738,6 +746,19 @@ const runTests = (dev = false) => { `You should not access 'res' after getServerSideProps resolves` ) }) + + it('should only warn for accessing res if not streaming', async () => { + const html = await renderViaHTTP( + appPort, + '/promise/mutate-res-no-streaming' + ) + expect(html).not.toContain( + `You should not access 'res' after getServerSideProps resolves` + ) + expect(stderr).toContain( + `You should not access 'res' after getServerSideProps resolves` + ) + }) } else { it('should not fetch data on mount', async () => { const browser = await webdriver(appPort, '/blog/post-100') @@ -800,6 +821,11 @@ const runTests = (dev = false) => { const html = await renderViaHTTP(appPort, '/promise/mutate-res') expect(html).toMatch(/hello.*?res/) }) + + it('should not warn for accessing res after gssp returns', async () => { + const html = await renderViaHTTP(appPort, '/promise/mutate-res') + expect(html).toMatch(/hello.*?res/) + }) } }