Skip to content

Commit

Permalink
Preview Mode Should Not Cache (#10636)
Browse files Browse the repository at this point in the history
* Preview Mode Should Not Cache

* add import

* fix import

* add tests

* Add real serverless mode tests
  • Loading branch information
Timer committed Feb 22, 2020
1 parent 0890b58 commit 89c792f
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 13 deletions.
15 changes: 13 additions & 2 deletions packages/next/build/webpack/loaders/next-serverless-loader.ts
Expand Up @@ -206,7 +206,8 @@ const nextServerlessLoader: loader.Loader = function() {
}
const {parse} = require('url')
const {parse: parseQs} = require('querystring')
const {renderToHTML} =require('next/dist/next-server/server/render');
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 buildManifest = require('${buildManifest}');
const reactLoadableManifest = require('${reactLoadableManifest}');
Expand Down Expand Up @@ -320,6 +321,9 @@ const nextServerlessLoader: loader.Loader = function() {
const isFallback = parsedUrl.query.__nextFallback
const previewData = tryGetPreviewData(req, res, options.previewProps)
const isPreviewMode = previewData !== false
let result = await renderToHTML(req, res, "${page}", Object.assign({}, unstable_getStaticProps ? {} : parsedUrl.query, nowParams ? nowParams : params, _params, isFallback ? { __nextFallback: 'true' } : {}), renderOpts)
if (_nextData && !fromExport) {
Expand All @@ -329,12 +333,19 @@ const nextServerlessLoader: loader.Loader = function() {
res.setHeader(
'Cache-Control',
unstable_getServerProps
isPreviewMode
? \`private, no-cache, no-store, max-age=0, must-revalidate\`
: unstable_getServerProps
? \`no-cache, no-store, must-revalidate\`
: \`s-maxage=\${renderOpts.revalidate}, stale-while-revalidate\`
)
res.end(payload)
return null
} else if (isPreviewMode) {
res.setHeader(
'Cache-Control',
'private, no-cache, no-store, max-age=0, must-revalidate'
)
}
if (fromExport) return { html: result, renderOpts }
Expand Down
31 changes: 22 additions & 9 deletions packages/next/next-server/server/next-server.ts
Expand Up @@ -821,20 +821,25 @@ export default class Server {
res: ServerResponse,
payload: any,
type: string,
revalidate?: number | false
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 (!this.renderOpts.dev) {
if (revalidate) {
if (options?.private) {
res.setHeader(
'Cache-Control',
revalidate < 0
`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=${revalidate}, stale-while-revalidate`
: `s-maxage=${options.revalidate}, stale-while-revalidate`
)
} else if (revalidate === false) {
} else if (options?.revalidate === false) {
res.setHeader(
'Cache-Control',
`s-maxage=31536000, stale-while-revalidate`
Expand Down Expand Up @@ -910,7 +915,10 @@ export default class Server {
res,
JSON.stringify(renderResult?.renderOpts?.pageData),
'application/json',
-1
{
revalidate: -1,
private: false, // Leave to user-land caching
}
)
return null
}
Expand All @@ -924,7 +932,10 @@ export default class Server {
...opts,
isDataReq,
})
this.__sendPayload(res, JSON.stringify(props), 'application/json', -1)
this.__sendPayload(res, JSON.stringify(props), 'application/json', {
revalidate: -1,
private: false, // Leave to user-land caching
})
return null
}

Expand Down Expand Up @@ -957,7 +968,9 @@ export default class Server {
res,
data,
isDataReq ? 'application/json' : 'text/html; charset=utf-8',
cachedData.curRevalidate
cachedData.curRevalidate !== undefined
? { revalidate: cachedData.curRevalidate, private: isPreviewMode }
: undefined
)

// Stop the request chain here if the data we sent was up-to-date
Expand Down Expand Up @@ -1064,7 +1077,7 @@ export default class Server {
res,
isDataReq ? JSON.stringify(pageData) : html,
isDataReq ? 'application/json' : 'text/html; charset=utf-8',
sprRevalidate
{ revalidate: sprRevalidate, private: isPreviewMode }
)
}

Expand Down
41 changes: 41 additions & 0 deletions test/integration/prerender-preview/server.js
@@ -0,0 +1,41 @@
const http = require('http')
const url = require('url')
const fs = require('fs')
const path = require('path')
const server = http.createServer((req, res) => {
let { pathname } = url.parse(req.url)
if (pathname.startsWith('/_next/data')) {
pathname = pathname
.replace(`/_next/data/${process.env.BUILD_ID}/`, '/')
.replace(/\.json$/, '')
}
console.log('serving', pathname)

if (pathname === '/favicon.ico') {
res.statusCode = 404
return res.end()
}

if (pathname.startsWith('/_next/static/')) {
res.write(
fs.readFileSync(
path.join(
__dirname,
'./.next/static/',
pathname.slice('/_next/static/'.length)
),
'utf8'
)
)
return res.end()
} else {
const re = require(`./.next/serverless/pages${pathname}`)
return typeof re.render === 'function'
? re.render(req, res)
: re.default(req, res)
}
})

server.listen(process.env.PORT, () => {
console.log('ready on', process.env.PORT)
})
57 changes: 55 additions & 2 deletions test/integration/prerender-preview/test/index.test.js
Expand Up @@ -6,6 +6,7 @@ import fs from 'fs-extra'
import {
fetchViaHTTP,
findPort,
initNextServerScript,
killApp,
nextBuild,
nextStart,
Expand All @@ -20,14 +21,18 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2
const appDir = join(__dirname, '..')
const nextConfigPath = join(appDir, 'next.config.js')

async function getBuildId() {
return fs.readFile(join(appDir, '.next', 'BUILD_ID'), 'utf8')
}

function getData(html) {
const $ = cheerio.load(html)
const nextData = $('#__NEXT_DATA__')
const preEl = $('#props-pre')
return { nextData: JSON.parse(nextData.html()), pre: preEl.text() }
}

function runTests() {
function runTests(startServer = nextStart) {
it('should compile successfully', async () => {
await fs.remove(join(appDir, '.next'))
const { code, stdout } = await nextBuild(appDir, [], {
Expand All @@ -40,7 +45,7 @@ function runTests() {
let appPort, app
it('should start production application', async () => {
appPort = await findPort()
app = await nextStart(appDir, appPort)
app = await startServer(appDir, appPort)
})

it('should return prerendered page on first request', async () => {
Expand Down Expand Up @@ -91,10 +96,33 @@ function runTests() {
const html = await res.text()

const { nextData, pre } = getData(html)
expect(res.headers.get('cache-control')).toBe(
'private, no-cache, no-store, max-age=0, must-revalidate'
)
expect(nextData).toMatchObject({ isFallback: false })
expect(pre).toBe('true and {"lets":"goooo"}')
})

it('should return correct caching headers for data preview request', async () => {
const res = await fetchViaHTTP(
appPort,
`/_next/data/${encodeURI(await getBuildId())}/index.json`,
{},
{ headers: { Cookie: previewCookieString } }
)
const json = await res.json()

expect(res.headers.get('cache-control')).toBe(
'private, no-cache, no-store, max-age=0, must-revalidate'
)
expect(json).toMatchObject({
pageProps: {
preview: true,
previewData: { lets: 'goooo' },
},
})
})

it('should return cookies to be expired on reset request', async () => {
const res = await fetchViaHTTP(
appPort,
Expand Down Expand Up @@ -162,6 +190,16 @@ function runTests() {
})
}

const startServerlessEmulator = async (dir, port) => {
const scriptPath = join(dir, 'server.js')
const env = Object.assign(
{},
{ ...process.env },
{ PORT: port, BUILD_ID: await getBuildId() }
)
return initNextServerScript(scriptPath, /ready on/i, env)
}

describe('Prerender Preview Mode', () => {
describe('Server Mode', () => {
beforeAll(async () => {
Expand All @@ -170,6 +208,7 @@ describe('Prerender Preview Mode', () => {

runTests()
})

describe('Serverless Mode', () => {
beforeAll(async () => {
await fs.writeFile(
Expand All @@ -183,4 +222,18 @@ describe('Prerender Preview Mode', () => {

runTests()
})

describe('Emulated Serverless Mode', () => {
beforeAll(async () => {
await fs.writeFile(
nextConfigPath,
`module.exports = { target: 'experimental-serverless-trace' }` + os.EOL
)
})
afterAll(async () => {
await fs.remove(nextConfigPath)
})

runTests(startServerlessEmulator)
})
})

0 comments on commit 89c792f

Please sign in to comment.