diff --git a/readme.md b/readme.md index 76a31ce9..f61c8323 100644 --- a/readme.md +++ b/readme.md @@ -1047,6 +1047,17 @@ module.exports = { } ``` +#### Disabling etag generation + +You can disable etag generation for HTML pages depending on your cache strategy. If no configuration is specified then Next will generate etags for every page. + +```js +// next.config.js +module.exports = { + generateEtags: false +} +``` + #### Configuring the onDemandEntries Next exposes some options that give you some control over how the server will dispose or keep in memories pages built: diff --git a/server/config.js b/server/config.js index 7dd5ba13..ecba94de 100644 --- a/server/config.js +++ b/server/config.js @@ -10,6 +10,7 @@ const defaultConfig = { assetPrefix: '', configOrigin: 'default', useFileSystemPublicRoutes: true, + generateEtags: true, pageExtensions: ['jsx', 'js'] // jsx before js because otherwise regex matching will match js first } diff --git a/server/index.js b/server/index.js index 49196374..92c14925 100644 --- a/server/index.js +++ b/server/index.js @@ -45,6 +45,10 @@ export default class Server { updateNotifier(pkg, 'next') } + // Only serverRuntimeConfig needs the default + // publicRuntimeConfig gets it's default in client/index.js + const {serverRuntimeConfig = {}, publicRuntimeConfig, assetPrefix, generateEtags} = this.nextConfig + if (!dev && !fs.existsSync(resolve(dir, this.dist, 'BUILD_ID'))) { console.error(`> Could not find a valid build in the '${this.dist}' directory! Try building your app with 'next build' before starting the server.`) process.exit(1) @@ -57,13 +61,10 @@ export default class Server { dist: this.dist, hotReloader: this.hotReloader, buildId: this.buildId, - availableChunks: dev ? {} : getAvailableChunks(this.dir, this.dist) + availableChunks: dev ? {} : getAvailableChunks(this.dir, this.dist), + generateEtags } - // Only serverRuntimeConfig needs the default - // publicRuntimeConfig gets it's default in client/index.js - const {serverRuntimeConfig = {}, publicRuntimeConfig, assetPrefix} = this.nextConfig - // Only the `publicRuntimeConfig` key is exposed to the client side // It'll be rendered as part of __NEXT_DATA__ on the client side if (publicRuntimeConfig) { diff --git a/server/render.js b/server/render.js index 0d868ac9..07a7ea8f 100644 --- a/server/render.js +++ b/server/render.js @@ -138,9 +138,9 @@ export async function renderScriptError (req, res, page, error) { res.end('500 - Internal Error') } -export function sendHTML (req, res, html, method, { dev }) { +export function sendHTML (req, res, html, method, { dev, generateEtags }) { if (isResSent(res)) return - const etag = generateETag(html) + const etag = generateEtags && generateETag(html) if (fresh(req.headers, { etag })) { res.statusCode = 304 @@ -154,7 +154,10 @@ export function sendHTML (req, res, html, method, { dev }) { res.setHeader('Cache-Control', 'no-store, must-revalidate') } - res.setHeader('ETag', etag) + if (etag) { + res.setHeader('ETag', etag) + } + if (!res.getHeader('Content-Type')) { res.setHeader('Content-Type', 'text/html; charset=utf-8') } diff --git a/test/integration/custom-server/next.config.js b/test/integration/custom-server/next.config.js index 35dcf0f6..4acb43c2 100644 --- a/test/integration/custom-server/next.config.js +++ b/test/integration/custom-server/next.config.js @@ -2,5 +2,6 @@ module.exports = { onDemandEntries: { // Make sure entries are not getting disposed. maxInactiveAge: 1000 * 60 * 60 - } + }, + generateEtags: process.env.GENERATE_ETAGS === 'true' } diff --git a/test/integration/custom-server/test/index.test.js b/test/integration/custom-server/test/index.test.js index 92b60631..6bcf36df 100644 --- a/test/integration/custom-server/test/index.test.js +++ b/test/integration/custom-server/test/index.test.js @@ -7,7 +7,8 @@ import cheerio from 'cheerio' import { initNextServerScript, killApp, - renderViaHTTP + renderViaHTTP, + fetchViaHTTP } from 'next-test-utils' import webdriver from 'next-webdriver' @@ -18,18 +19,24 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2 const context = {} +const startServer = async (optEnv = {}) => { + const scriptPath = join(appDir, 'server.js') + context.appPort = appPort = await getPort() + const env = Object.assign( + {}, + clone(process.env), + { PORT: `${appPort}` }, + optEnv + ) + + server = await initNextServerScript(scriptPath, /Ready on/, env) +} + describe('Custom Server', () => { - beforeAll(async () => { - const scriptPath = join(appDir, 'server.js') - context.appPort = appPort = await getPort() - const env = clone(process.env) - env.PORT = `${appPort}` - - server = await initNextServerScript(scriptPath, /Ready on/, env) - }) - afterAll(() => killApp(server)) - describe('with dynamic assetPrefix', () => { + beforeAll(() => startServer()) + afterAll(() => killApp(server)) + it('should set the assetPrefix dynamically', async () => { const normalUsage = await renderViaHTTP(appPort, '/asset') expect(normalUsage).not.toMatch(/127\.0\.0\.1/) @@ -83,4 +90,24 @@ describe('Custom Server', () => { browser2.close() }) }) + + describe('with generateEtags enabled', () => { + beforeAll(() => startServer({ GENERATE_ETAGS: 'true' })) + afterAll(() => killApp(server)) + + it('response includes etag header', async () => { + const response = await fetchViaHTTP(appPort, '/') + expect(response.headers.get('etag')).toBeTruthy() + }) + }) + + describe('with generateEtags disabled', () => { + beforeAll(() => startServer({ GENERATE_ETAGS: 'false' })) + afterAll(() => killApp(server)) + + it('response does not include etag header', async () => { + const response = await fetchViaHTTP(appPort, '/') + expect(response.headers.get('etag')).toBeNull() + }) + }) })