Skip to content

Commit f82e529

Browse files
authored
Implement ETag support for server rendered pages. (vercel#1693)
1 parent e0f71d8 commit f82e529

File tree

6 files changed

+40
-18
lines changed

6 files changed

+40
-18
lines changed

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@
6161
"case-sensitive-paths-webpack-plugin": "2.0.0",
6262
"cross-spawn": "5.1.0",
6363
"del": "2.2.2",
64+
"etag": "1.8.0",
65+
"fresh": "0.5.0",
6466
"friendly-errors-webpack-plugin": "1.5.0",
65-
"glob": "^7.1.1",
67+
"glob": "7.1.1",
6668
"glob-promise": "3.1.0",
6769
"htmlescape": "1.1.1",
6870
"http-status": "1.0.1",

server/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ export default class Server {
225225
res.setHeader('X-Powered-By', `Next.js ${pkg.version}`)
226226
}
227227
const html = await this.renderToHTML(req, res, pathname, query)
228-
return sendHTML(res, html, req.method)
228+
return sendHTML(req, res, html, req.method)
229229
}
230230

231231
async renderToHTML (req, res, pathname, query) {
@@ -253,7 +253,7 @@ export default class Server {
253253

254254
async renderError (err, req, res, pathname, query) {
255255
const html = await this.renderErrorToHTML(err, req, res, pathname, query)
256-
return sendHTML(res, html, req.method)
256+
return sendHTML(req, res, html, req.method)
257257
}
258258

259259
async renderErrorToHTML (err, req, res, pathname, query) {

server/render.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { join } from 'path'
22
import { createElement } from 'react'
33
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
44
import send from 'send'
5+
import generateETag from 'etag'
6+
import fresh from 'fresh'
57
import requireModule from './require'
68
import getConfig from './config'
79
import resolvePath from './resolve'
@@ -13,7 +15,7 @@ import ErrorDebug from '../lib/error-debug'
1315

1416
export async function render (req, res, pathname, query, opts) {
1517
const html = await renderToHTML(req, res, pathname, opts)
16-
sendHTML(res, html, req.method)
18+
sendHTML(req, res, html, req.method)
1719
}
1820

1921
export function renderToHTML (req, res, pathname, query, opts) {
@@ -22,7 +24,7 @@ export function renderToHTML (req, res, pathname, query, opts) {
2224

2325
export async function renderError (err, req, res, pathname, query, opts) {
2426
const html = await renderErrorToHTML(err, req, res, query, opts)
25-
sendHTML(res, html, req.method)
27+
sendHTML(req, res, html, req.method)
2628
}
2729

2830
export function renderErrorToHTML (err, req, res, pathname, query, opts = {}) {
@@ -148,9 +150,17 @@ export async function renderScriptError (req, res, page, error, customFields, op
148150
`)
149151
}
150152

151-
export function sendHTML (res, html, method) {
153+
export function sendHTML (req, res, html, method) {
152154
if (res.finished) return
155+
const etag = generateETag(html)
153156

157+
if (fresh(req.headers, { etag })) {
158+
res.statusCode = 304
159+
res.end()
160+
return
161+
}
162+
163+
res.setHeader('ETag', etag)
154164
res.setHeader('Content-Type', 'text/html')
155165
res.setHeader('Content-Length', Buffer.byteLength(html))
156166
res.end(method === 'HEAD' ? null : html)

test/integration/basic/test/misc.js

+10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* global describe, test, expect */
2+
import fetch from 'node-fetch'
23

34
export default function (context) {
45
describe('Misc', () => {
@@ -12,5 +13,14 @@ export default function (context) {
1213
const html = await context.app.renderToHTML({}, res, '/finish-response', {})
1314
expect(html).toBeFalsy()
1415
})
16+
17+
test('allow etag header support', async () => {
18+
const url = `http://localhost:${context.appPort}/stateless`
19+
const etag = (await fetch(url)).headers.get('ETag')
20+
21+
const headers = { 'If-None-Match': etag }
22+
const res2 = await fetch(url, { headers })
23+
expect(res2.status).toBe(304)
24+
})
1525
})
1626
}

test/integration/basic/test/xpowered-by.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { pkg } from 'next-test-utils'
44
export default function ({ app }) {
55
describe('X-Powered-By header', () => {
66
test('set it by default', async () => {
7-
const req = { url: '/stateless' }
7+
const req = { url: '/stateless', headers: {} }
88
const headers = {}
99
const res = {
1010
setHeader (key, value) {
@@ -18,7 +18,7 @@ export default function ({ app }) {
1818
})
1919

2020
test('do not set it when poweredByHeader==false', async () => {
21-
const req = { url: '/stateless' }
21+
const req = { url: '/stateless', headers: {} }
2222
const originalConfigValue = app.config.poweredByHeader
2323
app.config.poweredByHeader = false
2424
const res = {

yarn.lock

+10-10
Original file line numberDiff line numberDiff line change
@@ -1972,7 +1972,7 @@ esutils@^2.0.0, esutils@^2.0.2:
19721972
version "2.0.2"
19731973
resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
19741974

1975-
etag@~1.8.0:
1975+
etag@1.8.0, etag@~1.8.0:
19761976
version "1.8.0"
19771977
resolved "https://registry.npmjs.org/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051"
19781978

@@ -2331,24 +2331,24 @@ glob-promise@3.1.0:
23312331
version "3.1.0"
23322332
resolved "https://registry.npmjs.org/glob-promise/-/glob-promise-3.1.0.tgz#198882a3817be7dc2c55f92623aa9e7b3f82d1eb"
23332333

2334-
glob@^6.0.1:
2335-
version "6.0.4"
2336-
resolved "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
2334+
glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1:
2335+
version "7.1.1"
2336+
resolved "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
23372337
dependencies:
2338+
fs.realpath "^1.0.0"
23382339
inflight "^1.0.4"
23392340
inherits "2"
2340-
minimatch "2 || 3"
2341+
minimatch "^3.0.2"
23412342
once "^1.3.0"
23422343
path-is-absolute "^1.0.0"
23432344

2344-
glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1:
2345-
version "7.1.1"
2346-
resolved "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
2345+
glob@^6.0.1:
2346+
version "6.0.4"
2347+
resolved "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
23472348
dependencies:
2348-
fs.realpath "^1.0.0"
23492349
inflight "^1.0.4"
23502350
inherits "2"
2351-
minimatch "^3.0.2"
2351+
minimatch "2 || 3"
23522352
once "^1.3.0"
23532353
path-is-absolute "^1.0.0"
23542354

0 commit comments

Comments
 (0)