Skip to content

Commit

Permalink
Correct Cache-Control Behavior for GS(S)P (#11022)
Browse files Browse the repository at this point in the history
* Correct Cache-Control Behavior for GS(S)P

* remove old line

* fix test
  • Loading branch information
Timer committed Mar 13, 2020
1 parent 6997b02 commit 18036d4
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 67 deletions.
25 changes: 10 additions & 15 deletions packages/next/build/webpack/loaders/next-serverless-loader.ts
Expand Up @@ -205,6 +205,7 @@ const nextServerlessLoader: loader.Loader = function() {
const {renderToHTML} = require('next/dist/next-server/server/render');
const { tryGetPreviewData } = require('next/dist/next-server/server/api-utils');
const {sendHTML} = require('next/dist/next-server/server/send-html');
const {sendPayload} = require('next/dist/next-server/server/send-payload');
const buildManifest = require('${buildManifest}');
const reactLoadableManifest = require('${reactLoadableManifest}');
const Document = require('${absoluteDocumentPath}').default;
Expand Down Expand Up @@ -328,21 +329,15 @@ const nextServerlessLoader: loader.Loader = function() {
let result = await renderToHTML(req, res, "${page}", Object.assign({}, getStaticProps ? {} : parsedUrl.query, nowParams ? nowParams : params, _params, isFallback ? { __nextFallback: 'true' } : {}), renderOpts)
if (_nextData && !renderMode) {
const payload = JSON.stringify(renderOpts.pageData)
res.setHeader('Content-Type', 'application/json')
res.setHeader('Content-Length', Buffer.byteLength(payload))
res.setHeader(
'Cache-Control',
isPreviewMode
? \`private, no-cache, no-store, max-age=0, must-revalidate\`
: getServerSideProps
? \`no-cache, no-store, must-revalidate\`
: \`s-maxage=\${renderOpts.revalidate}, stale-while-revalidate\`
)
res.end(payload)
return null
if (!renderMode) {
if (_nextData || getStaticProps || getServerSideProps) {
sendPayload(res, _nextData ? JSON.stringify(renderOpts.pageData) : result, _nextData ? 'json' : 'html', {
private: isPreviewMode,
stateful: !!getServerSideProps,
revalidate: renderOpts.revalidate,
})
return null
}
} else if (isPreviewMode) {
res.setHeader(
'Cache-Control',
Expand Down
82 changes: 34 additions & 48 deletions packages/next/next-server/server/next-server.ts
Expand Up @@ -52,6 +52,7 @@ import Router, {
Route,
} from './router'
import { sendHTML } from './send-html'
import { sendPayload } from './send-payload'
import { serveStatic } from './serve-static'
import {
getFallback,
Expand Down Expand Up @@ -932,11 +933,11 @@ export default class Server {
sendPayload(
res,
JSON.stringify(renderResult?.renderOpts?.pageData),
'application/json',
'json',
!this.renderOpts.dev
? {
revalidate: -1,
private: isPreviewMode, // Leave to user-land caching
private: isPreviewMode,
stateful: true, // non-SSG data request
}
: undefined
)
Expand All @@ -955,11 +956,11 @@ export default class Server {
sendPayload(
res,
JSON.stringify(props),
'application/json',
'json',
!this.renderOpts.dev
? {
revalidate: -1,
private: isPreviewMode, // Leave to user-land caching
private: isPreviewMode,
stateful: true, // GSSP data request
}
: undefined
)
Expand All @@ -971,11 +972,12 @@ export default class Server {
...opts,
})

if (html && isServerProps && isPreviewMode) {
sendPayload(res, html, 'text/html; charset=utf-8', {
revalidate: -1,
if (html && isServerProps) {
sendPayload(res, html, 'html', {
private: isPreviewMode,
stateful: true, // GSSP request
})
return null
}

return html
Expand All @@ -1000,9 +1002,16 @@ export default class Server {
sendPayload(
res,
data,
isDataReq ? 'application/json' : 'text/html; charset=utf-8',
cachedData.curRevalidate !== undefined && !this.renderOpts.dev
? { revalidate: cachedData.curRevalidate, private: isPreviewMode }
isDataReq ? 'json' : 'html',
!this.renderOpts.dev
? {
private: isPreviewMode,
stateful: false, // GSP response
revalidate:
cachedData.curRevalidate !== undefined
? cachedData.curRevalidate
: /* default to minimum revalidate (this should be an invariant) */ 1,
}
: undefined
)

Expand Down Expand Up @@ -1105,7 +1114,12 @@ export default class Server {
query.__nextFallback = 'true'
if (isLikeServerless) {
prepareServerlessUrl(req, query)
html = await (components.Component as any).renderReqToHTML(req, res)
const renderResult = await (components.Component as any).renderReqToHTML(
req,
res,
'passthrough'
)
html = renderResult.html
} else {
html = (await renderToHTML(req, res, pathname, query, {
...components,
Expand All @@ -1114,7 +1128,7 @@ export default class Server {
}
}

sendPayload(res, html, 'text/html; charset=utf-8')
sendPayload(res, html, 'html')
}

const {
Expand All @@ -1125,9 +1139,13 @@ export default class Server {
sendPayload(
res,
isDataReq ? JSON.stringify(pageData) : html,
isDataReq ? 'application/json' : 'text/html; charset=utf-8',
isDataReq ? 'json' : 'html',
!this.renderOpts.dev
? { revalidate: sprRevalidate, private: isPreviewMode }
? {
private: isPreviewMode,
stateful: false, // GSP response
revalidate: sprRevalidate,
}
: undefined
)
}
Expand Down Expand Up @@ -1348,38 +1366,6 @@ export default class Server {
}
}

function sendPayload(
res: ServerResponse,
payload: any,
type: string,
options?: { revalidate: number | false; private: boolean }
) {
// TODO: ETag? Cache-Control headers? Next-specific headers?
res.setHeader('Content-Type', type)
res.setHeader('Content-Length', Buffer.byteLength(payload))
if (options != null) {
if (options?.private) {
res.setHeader(
'Cache-Control',
`private, no-cache, no-store, max-age=0, must-revalidate`
)
} else if (options?.revalidate) {
res.setHeader(
'Cache-Control',
options.revalidate < 0
? `no-cache, no-store, must-revalidate`
: `s-maxage=${options.revalidate}, stale-while-revalidate`
)
} else if (options?.revalidate === false) {
res.setHeader(
'Cache-Control',
`s-maxage=31536000, stale-while-revalidate`
)
}
}
res.end(payload)
}

function prepareServerlessUrl(req: IncomingMessage, query: ParsedUrlQuery) {
const curUrl = parseUrl(req.url!, true)
req.url = formatUrl({
Expand Down
50 changes: 50 additions & 0 deletions packages/next/next-server/server/send-payload.ts
@@ -0,0 +1,50 @@
import { ServerResponse } from 'http'
import { isResSent } from '../lib/utils'

export function sendPayload(
res: ServerResponse,
payload: any,
type: 'html' | 'json',
options?:
| { private: true }
| { private: boolean; stateful: true }
| { private: boolean; stateful: false; revalidate: number | false }
): void {
if (isResSent(res)) {
return
}

// TODO: ETag headers?
res.setHeader(
'Content-Type',
type === 'json' ? 'application/json' : 'text/html; charset=utf-8'
)
res.setHeader('Content-Length', Buffer.byteLength(payload))
if (options != null) {
if (options.private || options.stateful) {
if (options.private || !res.hasHeader('Cache-Control')) {
res.setHeader(
'Cache-Control',
`private, no-cache, no-store, max-age=0, must-revalidate`
)
}
} else if (typeof options.revalidate === 'number') {
if (options.revalidate < 1) {
throw new Error(
`invariant: invalid Cache-Control duration provided: ${options.revalidate} < 1`
)
}

res.setHeader(
'Cache-Control',
`s-maxage=${options.revalidate}, stale-while-revalidate`
)
} else if (options.revalidate === false) {
res.setHeader(
'Cache-Control',
`s-maxage=31536000, stale-while-revalidate`
)
}
}
res.end(payload)
}
4 changes: 3 additions & 1 deletion test/integration/getserversideprops-preview/pages/index.js
@@ -1,4 +1,6 @@
export function getServerSideProps({ preview, previewData }) {
export function getServerSideProps({ res, preview, previewData }) {
// test override in preview mode
res.setHeader('Cache-Control', 'public, max-age=3600')
return {
props: {
hasProps: true,
Expand Down
12 changes: 12 additions & 0 deletions test/integration/getserversideprops/pages/custom-cache.js
@@ -0,0 +1,12 @@
import React from 'react'

export async function getServerSideProps({ res }) {
res.setHeader('Cache-Control', 'public, max-age=3600')
return {
props: { world: 'world' },
}
}

export default ({ world }) => {
return <p>hello: {world}</p>
}
30 changes: 27 additions & 3 deletions test/integration/getserversideprops/test/index.test.js
Expand Up @@ -66,6 +66,12 @@ const expectedManifestRoutes = () => [
),
page: '/catchall/[...path]',
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(buildId)}\\/custom-cache.json$`
),
page: '/custom-cache',
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(buildId)}\\/default-revalidate.json$`
Expand Down Expand Up @@ -414,12 +420,30 @@ const runTests = (dev = false) => {
expect(dataRoutes).toEqual(expectedManifestRoutes())
})

it('should set no-cache, no-store, must-revalidate header', async () => {
const res = await fetchViaHTTP(
it('should set default caching header', async () => {
const resPage = await fetchViaHTTP(appPort, `/something`)
expect(resPage.headers.get('cache-control')).toBe(
'private, no-cache, no-store, max-age=0, must-revalidate'
)

const resData = await fetchViaHTTP(
appPort,
`/_next/data/${buildId}/something.json`
)
expect(res.headers.get('cache-control')).toContain('no-cache')
expect(resData.headers.get('cache-control')).toBe(
'private, no-cache, no-store, max-age=0, must-revalidate'
)
})

it('should respect custom caching header', async () => {
const resPage = await fetchViaHTTP(appPort, `/custom-cache`)
expect(resPage.headers.get('cache-control')).toBe('public, max-age=3600')

const resData = await fetchViaHTTP(
appPort,
`/_next/data/${buildId}/custom-cache.json`
)
expect(resData.headers.get('cache-control')).toBe('public, max-age=3600')
})

it('should not show error for invalid JSON returned from getServerSideProps', async () => {
Expand Down

0 comments on commit 18036d4

Please sign in to comment.