Skip to content

Commit

Permalink
Warn when mutating res if not streaming
Browse files Browse the repository at this point in the history
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](#29010 (comment)) 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.
  • Loading branch information
kara committed Oct 25, 2021
1 parent f5b7bac commit 0a57076
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 4 deletions.
2 changes: 2 additions & 0 deletions errors/gssp-no-mutating-res.md
Expand Up @@ -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)
18 changes: 14 additions & 4 deletions packages/next/server/render.tsx
Expand Up @@ -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<ServerResponse>(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)
},
Expand Down Expand Up @@ -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'
)
Expand Down Expand Up @@ -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
}

Expand Down
1 change: 1 addition & 0 deletions test/integration/data-fetching-errors/pages/index.js
@@ -1,2 +1,3 @@
const Page = () => 'hi'
Page.getStaticPaths = () => ({ paths: [], fallback: true })
export default Page
@@ -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 (
<>
<div>hello {text}</div>
</>
)
}
26 changes: 26 additions & 0 deletions test/integration/getserversideprops/test/index.test.js
Expand Up @@ -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(
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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/)
})
}
}

Expand Down

0 comments on commit 0a57076

Please sign in to comment.