From f82e52935db5bea04c5eb0f7328c06618d62a235 Mon Sep 17 00:00:00 2001 From: Arunoda Susiripala Date: Tue, 18 Apr 2017 21:15:42 +0530 Subject: [PATCH] Implement ETag support for server rendered pages. (#1693) --- package.json | 4 +++- server/index.js | 4 ++-- server/render.js | 16 ++++++++++++--- test/integration/basic/test/misc.js | 10 +++++++++ test/integration/basic/test/xpowered-by.js | 4 ++-- yarn.lock | 24 +++++++++++----------- 6 files changed, 42 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index d61b0c06..077ed82c 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,10 @@ "case-sensitive-paths-webpack-plugin": "2.0.0", "cross-spawn": "5.1.0", "del": "2.2.2", + "etag": "1.8.0", + "fresh": "0.5.0", "friendly-errors-webpack-plugin": "1.5.0", - "glob": "^7.1.1", + "glob": "7.1.1", "glob-promise": "3.1.0", "htmlescape": "1.1.1", "http-status": "1.0.1", diff --git a/server/index.js b/server/index.js index 45751abd..ffa59e64 100644 --- a/server/index.js +++ b/server/index.js @@ -225,7 +225,7 @@ export default class Server { res.setHeader('X-Powered-By', `Next.js ${pkg.version}`) } const html = await this.renderToHTML(req, res, pathname, query) - return sendHTML(res, html, req.method) + return sendHTML(req, res, html, req.method) } async renderToHTML (req, res, pathname, query) { @@ -253,7 +253,7 @@ export default class Server { async renderError (err, req, res, pathname, query) { const html = await this.renderErrorToHTML(err, req, res, pathname, query) - return sendHTML(res, html, req.method) + return sendHTML(req, res, html, req.method) } async renderErrorToHTML (err, req, res, pathname, query) { diff --git a/server/render.js b/server/render.js index 8c3dd264..04b4a5ce 100644 --- a/server/render.js +++ b/server/render.js @@ -2,6 +2,8 @@ import { join } from 'path' import { createElement } from 'react' import { renderToString, renderToStaticMarkup } from 'react-dom/server' import send from 'send' +import generateETag from 'etag' +import fresh from 'fresh' import requireModule from './require' import getConfig from './config' import resolvePath from './resolve' @@ -13,7 +15,7 @@ import ErrorDebug from '../lib/error-debug' export async function render (req, res, pathname, query, opts) { const html = await renderToHTML(req, res, pathname, opts) - sendHTML(res, html, req.method) + sendHTML(req, res, html, req.method) } export function renderToHTML (req, res, pathname, query, opts) { @@ -22,7 +24,7 @@ export function renderToHTML (req, res, pathname, query, opts) { export async function renderError (err, req, res, pathname, query, opts) { const html = await renderErrorToHTML(err, req, res, query, opts) - sendHTML(res, html, req.method) + sendHTML(req, res, html, req.method) } export function renderErrorToHTML (err, req, res, pathname, query, opts = {}) { @@ -148,9 +150,17 @@ export async function renderScriptError (req, res, page, error, customFields, op `) } -export function sendHTML (res, html, method) { +export function sendHTML (req, res, html, method) { if (res.finished) return + const etag = generateETag(html) + if (fresh(req.headers, { etag })) { + res.statusCode = 304 + res.end() + return + } + + res.setHeader('ETag', etag) res.setHeader('Content-Type', 'text/html') res.setHeader('Content-Length', Buffer.byteLength(html)) res.end(method === 'HEAD' ? null : html) diff --git a/test/integration/basic/test/misc.js b/test/integration/basic/test/misc.js index 29926790..3eb1e602 100644 --- a/test/integration/basic/test/misc.js +++ b/test/integration/basic/test/misc.js @@ -1,4 +1,5 @@ /* global describe, test, expect */ +import fetch from 'node-fetch' export default function (context) { describe('Misc', () => { @@ -12,5 +13,14 @@ export default function (context) { const html = await context.app.renderToHTML({}, res, '/finish-response', {}) expect(html).toBeFalsy() }) + + test('allow etag header support', async () => { + const url = `http://localhost:${context.appPort}/stateless` + const etag = (await fetch(url)).headers.get('ETag') + + const headers = { 'If-None-Match': etag } + const res2 = await fetch(url, { headers }) + expect(res2.status).toBe(304) + }) }) } diff --git a/test/integration/basic/test/xpowered-by.js b/test/integration/basic/test/xpowered-by.js index d0108b0b..253fbc1d 100644 --- a/test/integration/basic/test/xpowered-by.js +++ b/test/integration/basic/test/xpowered-by.js @@ -4,7 +4,7 @@ import { pkg } from 'next-test-utils' export default function ({ app }) { describe('X-Powered-By header', () => { test('set it by default', async () => { - const req = { url: '/stateless' } + const req = { url: '/stateless', headers: {} } const headers = {} const res = { setHeader (key, value) { @@ -18,7 +18,7 @@ export default function ({ app }) { }) test('do not set it when poweredByHeader==false', async () => { - const req = { url: '/stateless' } + const req = { url: '/stateless', headers: {} } const originalConfigValue = app.config.poweredByHeader app.config.poweredByHeader = false const res = { diff --git a/yarn.lock b/yarn.lock index 0160a00a..4e1e6cef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1972,7 +1972,7 @@ esutils@^2.0.0, esutils@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" -etag@~1.8.0: +etag@1.8.0, etag@~1.8.0: version "1.8.0" resolved "https://registry.npmjs.org/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" @@ -2331,17 +2331,7 @@ glob-promise@3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/glob-promise/-/glob-promise-3.1.0.tgz#198882a3817be7dc2c55f92623aa9e7b3f82d1eb" -glob@^6.0.1: - version "6.0.4" - resolved "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1: +glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1: version "7.1.1" resolved "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" dependencies: @@ -2352,6 +2342,16 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^6.0.1: + version "6.0.4" + resolved "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + global@^4.3.0: version "4.3.1" resolved "https://registry.npmjs.org/global/-/global-4.3.1.tgz#5f757908c7cbabce54f386ae440e11e26b7916df"