From 0a57076421716cbd1422fd5c40acc9b2746c678d Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Mon, 25 Oct 2021 14:58:38 -0700 Subject: [PATCH] Warn when mutating res if not streaming In #29010, we started throwing an error if the res was mutated after getServerSideProps() returned. This was to support classic streaming, where it would be possible to accidentally mutate the response headers after they were already sent. However, this change also caught [a few non-streaming cases](https://github.com/vercel/next.js/pull/29010#issuecomment-943482743) that we don't want to break. As such, with this change, we only throw the error if res is mutated after gSSP returns *and* you've opted into using classic streaming. Otherwise, we will only add a warning to the console. --- errors/gssp-no-mutating-res.md | 2 ++ packages/next/server/render.tsx | 18 ++++++++++--- .../data-fetching-errors/pages/index.js | 1 + .../pages/promise/mutate-res-no-streaming.js | 20 ++++++++++++++ .../getserversideprops/test/index.test.js | 26 +++++++++++++++++++ 5 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 test/integration/getserversideprops/pages/promise/mutate-res-no-streaming.js 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/data-fetching-errors/pages/index.js b/test/integration/data-fetching-errors/pages/index.js index 410f652f8018bf5..62d42778c06d1d4 100644 --- a/test/integration/data-fetching-errors/pages/index.js +++ b/test/integration/data-fetching-errors/pages/index.js @@ -1,2 +1,3 @@ const Page = () => 'hi' +Page.getStaticPaths = () => ({ paths: [], fallback: true }) export default Page 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/) + }) } }