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

Lines changed: 3 additions & 1 deletion
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 13 additions & 3 deletions
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

Lines changed: 10 additions & 0 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 10 additions & 10 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy