From d575b8edd63fbaa907bb61f04d2627b483fd5317 Mon Sep 17 00:00:00 2001 From: brannon Date: Thu, 2 Jun 2022 17:08:34 -0600 Subject: [PATCH 1/3] Update Fastly test middleware to run in staging ONLY. Add ability to set more headers to mimic real content responses. Add ability to inject errors, to help in validating behavior. --- middleware/fastly-behavior.js | 14 +++++++++ middleware/fastly-cache-test.js | 56 +++++++++++++++++++++++++++------ middleware/index.js | 17 +++++++--- 3 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 middleware/fastly-behavior.js diff --git a/middleware/fastly-behavior.js b/middleware/fastly-behavior.js new file mode 100644 index 0000000000..f4f8a5f728 --- /dev/null +++ b/middleware/fastly-behavior.js @@ -0,0 +1,14 @@ +// This middleware allows the client to cause the server-side processing to fail with a specific error. +// It is used for testing error handling with Fastly. It should only be enabled in non-production environments! +// +// NOTE: This middleware is intended to be removed once testing is complete! +// +export default function fastlyBehavior(req, res, next) { + if ((req.method === 'GET' || req.method === 'HEAD') && req.get('X-CacheTest-Error')) { + const error = parseInt(req.get('X-CacheTest-Error')) + res.status(error).send(`SIMULATED ERROR ${error}`) + return + } + + next() +} diff --git a/middleware/fastly-cache-test.js b/middleware/fastly-cache-test.js index 0ba8b28c91..306c3e6ff4 100644 --- a/middleware/fastly-cache-test.js +++ b/middleware/fastly-cache-test.js @@ -7,21 +7,36 @@ // // NOTE: This middleware is intended to be removed once testing is complete! // -export default function fastlyCacheTest(req, res, next) { +import express from 'express' +import crypto from 'crypto' + +const router = express.Router() + +router.get('/*', function (req, res) { // If X-CacheTest-Error is set, simulate the site being down (regardless of URL) if (req.get('X-CacheTest-Error')) { res.status(parseInt(req.get('X-CacheTest-Error'))).end() return } + const cacheControlModeParam = req.get('X-CacheTest-CCMode') ?? 'none' const staleIfErrorParam = req.get('X-CacheTest-StaleIfError') ?? '300' const staleWhileRevalidateParam = req.get('X-CacheTest-StaleWhileRevalidate') ?? '60' + const maxAgeParam = req.get('X-CacheTest-MaxAge') ?? '300' const path = req.params[0] + const content = ` + +

Timestamp: ${new Date()}

+ +` + let status = 200 const surrogateControlValues = [] + if (path.includes('max-age')) surrogateControlValues.push(`max-age=${maxAgeParam}`) + if (path.includes('must-revalidate')) surrogateControlValues.push('must-revalidate') if (path.includes('no-cache')) surrogateControlValues.push('no-cache') if (path.includes('no-store')) surrogateControlValues.push('no-store') @@ -38,6 +53,12 @@ export default function fastlyCacheTest(req, res, next) { res.removeHeader('Set-Cookie') } + if (path.includes('etag')) { + res.set({ + ETag: `"${crypto.createHash('md5').update(content).digest('hex')}"`, + }) + } + if (path.includes('error500')) { status = 500 } else if (path.includes('error502')) { @@ -49,16 +70,31 @@ export default function fastlyCacheTest(req, res, next) { } if (surrogateControlValues.length > 0) { - res.set({ - 'surrogate-control': surrogateControlValues.join(', '), - }) + switch (cacheControlModeParam) { + case 'none': + res.set({ + 'surrogate-control': surrogateControlValues.join(', '), + }) + break + + case 'both': + res.set({ + 'surrogate-control': surrogateControlValues.join(', '), + 'cache-control': surrogateControlValues.join(', ').replace('max-age', 's-maxage'), + }) + break + + case 'only': + res.set({ + 'cache-control': surrogateControlValues.join(', ').replace('max-age', 's-maxage'), + }) + break + } } res.status(status) - res.send(` - -

Timestamp: ${new Date()}

- -`) + res.send(content) res.end() -} +}) + +export default router diff --git a/middleware/index.js b/middleware/index.js index 6a2b76b2c4..65ad0010d0 100644 --- a/middleware/index.js +++ b/middleware/index.js @@ -67,10 +67,12 @@ import fastHead from './fast-head.js' import fastlyCacheTest from './fastly-cache-test.js' import fastRootRedirect from './fast-root-redirect.js' import trailingSlashes from './trailing-slashes.js' +import fastlyBehavior from './fastly-behavior.js' const { DEPLOYMENT_ENV, NODE_ENV } = process.env const isDevelopment = NODE_ENV === 'development' const isAzureDeployment = DEPLOYMENT_ENV === 'azure' +const isStaging = process.env.HEROKU_APP_NAME === 'help-docs-staging' const isTest = NODE_ENV === 'test' || process.env.GITHUB_ACTIONS === 'true' // Catch unhandled promise rejections and passing them to Express's error handler @@ -244,6 +246,11 @@ export default function (app) { app.use(csp) // Must come after helmet app.use(cookieParser) // Must come before csrf app.use(express.json()) // Must come before csrf + + if (isStaging) { + app.use(fastlyBehavior) // FOR TESTING. Must come before csrf + } + app.use(csrf) app.use(handleCsrfErrors) // Must come before regular handle-errors @@ -324,10 +331,12 @@ export default function (app) { app.use(asyncMiddleware(instrument(featuredLinks, './featured-links'))) app.use(asyncMiddleware(instrument(learningTrack, './learning-track'))) - // The fastlyCacheTest middleware is intended to be used with Fastly to test caching behavior. - // This middleware will intercept ALL requests routed to it, so be careful if you need to - // make any changes to the following line: - app.use('/fastly-cache-test/*', fastlyCacheTest) + if (isStaging) { + // The fastlyCacheTest middleware is intended to be used with Fastly to test caching behavior. + // This middleware will intercept ALL requests routed to it, so be careful if you need to + // make any changes to the following line: + app.use('/fastly-cache-test', fastlyCacheTest) + } // *** Headers for pages only *** app.use(setFastlyCacheHeaders) From a1d48880c9a6c24aaef4c3f97d199f4150f8092f Mon Sep 17 00:00:00 2001 From: brannon Date: Thu, 2 Jun 2022 17:10:05 -0600 Subject: [PATCH 2/3] Disable NextJS ETag generation. --- next.config.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/next.config.js b/next.config.js index 9ab2fa2ff0..b0190cbead 100644 --- a/next.config.js +++ b/next.config.js @@ -42,4 +42,10 @@ module.exports = { // https://nextjs.org/docs/api-reference/next.config.js/compression compress: false, + + // ETags break stale content serving from the CDN. When a response has + // an ETag, the CDN attempts to revalidate the content in the background. + // This causes problems with serving stale content, since upon revalidating + // the CDN marks the cached content as "fresh". + generateEtags: false, } From 000a34a1a79b573ee605a0d19618e2e4d54878ef Mon Sep 17 00:00:00 2001 From: brannon Date: Fri, 3 Jun 2022 09:52:02 -0600 Subject: [PATCH 3/3] Add explicit config value to enable Fastly testing. --- middleware/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/middleware/index.js b/middleware/index.js index 34d04f01a7..eec5ffd683 100644 --- a/middleware/index.js +++ b/middleware/index.js @@ -69,9 +69,10 @@ import fastlyBehavior from './fastly-behavior.js' const { DEPLOYMENT_ENV, NODE_ENV } = process.env const isAzureDeployment = DEPLOYMENT_ENV === 'azure' -const isStaging = process.env.HEROKU_APP_NAME === 'help-docs-staging' const isTest = NODE_ENV === 'test' || process.env.GITHUB_ACTIONS === 'true' +const ENABLE_FASTLY_TESTING = JSON.parse(process.env.ENABLE_FASTLY_TESTING || 'false') + // Catch unhandled promise rejections and passing them to Express's error handler // https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016 const asyncMiddleware = (fn) => (req, res, next) => { @@ -221,7 +222,7 @@ export default function (app) { app.use(cookieParser) // Must come before csrf app.use(express.json()) // Must come before csrf - if (isStaging) { + if (ENABLE_FASTLY_TESTING) { app.use(fastlyBehavior) // FOR TESTING. Must come before csrf } @@ -303,7 +304,7 @@ export default function (app) { app.use(asyncMiddleware(instrument(featuredLinks, './featured-links'))) app.use(asyncMiddleware(instrument(learningTrack, './learning-track'))) - if (isStaging) { + if (ENABLE_FASTLY_TESTING) { // The fastlyCacheTest middleware is intended to be used with Fastly to test caching behavior. // This middleware will intercept ALL requests routed to it, so be careful if you need to // make any changes to the following line: