diff --git a/lib/api/middlewares/cache-control.js b/lib/api/middlewares/cache-control.js index 898b1ff2..002ec357 100644 --- a/lib/api/middlewares/cache-control.js +++ b/lib/api/middlewares/cache-control.js @@ -1,14 +1,36 @@ 'use strict'; -const ONE_YEAR_IN_SECONDS = 31536000; // ttl in cache provider -const FIVE_MINUTES_IN_SECONDS = 60 * 5; // ttl in cache provider +const ONE_MINUTE_IN_SECONDS = 60; +const THREE_MINUTE_IN_SECONDS = 60 * 3; +const FIVE_MINUTES_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 5; +const TEN_MINUTES_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 10; +const FIFTEEN_MINUTES_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 15; +const THIRTY_MINUTES_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 30; +const ONE_HOUR_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 60; +const ONE_YEAR_IN_SECONDS = ONE_HOUR_IN_SECONDS * 24 * 365; + const defaultCacheTTL = { ttl: ONE_YEAR_IN_SECONDS, fallbackTtl: FIVE_MINUTES_IN_SECONDS }; -const cacheControl = Object.assign(defaultCacheTTL, global.settings.cache); + +const validFallbackTTL = [ + ONE_MINUTE_IN_SECONDS, + THREE_MINUTE_IN_SECONDS, + FIVE_MINUTES_IN_SECONDS, + TEN_MINUTES_IN_SECONDS, + FIFTEEN_MINUTES_IN_SECONDS, + THIRTY_MINUTES_IN_SECONDS, + ONE_HOUR_IN_SECONDS +]; + +const { ttl, fallbackTtl } = Object.assign(defaultCacheTTL, global.settings.cache); module.exports = function cacheControlHeader () { + if (!validFallbackTTL.includes(fallbackTtl)) { + throw new Error(`Invalid fallback TTL value for Cache-Control header. Got ${fallbackTtl}, expected ${validFallbackTTL.join(', ')}`); + } + return function cacheControlHeaderMiddleware (req, res, next) { const { cachePolicy } = res.locals.params; const { affectedTables, mayWrite } = res.locals; @@ -20,15 +42,23 @@ module.exports = function cacheControlHeader () { } if (affectedTables && affectedTables.getTables().every(table => table.updated_at !== null)) { - const maxAge = mayWrite ? 0 : cacheControl.ttl; + const maxAge = mayWrite ? 0 : ttl; res.header('Cache-Control', `no-cache,max-age=${maxAge},must-revalidate,public`); return next(); } - const maxAge = cacheControl.fallbackTtl; - res.header('Cache-Control', `no-cache,max-age=${maxAge},must-revalidate,public`); + const maxAge = fallbackTtl; + res.header('Cache-Control', `no-cache,max-age=${computeNextTTL({ ttlInSeconds: maxAge })},must-revalidate,public`); return next(); }; }; + +function computeNextTTL ({ ttlInSeconds } = {}) { + const nowInSeconds = Math.ceil(Date.now() / 1000); + const secondsAfterPreviousTTLStep = nowInSeconds % ttlInSeconds; + const secondsToReachTheNextTTLStep = ttlInSeconds - secondsAfterPreviousTTLStep; + + return secondsToReachTheNextTTLStep; +} diff --git a/test/acceptance/app-test.js b/test/acceptance/app-test.js index 12bd6301..38af31e8 100644 --- a/test/acceptance/app-test.js +++ b/test/acceptance/app-test.js @@ -246,7 +246,14 @@ it('TRUNCATE TABLE with GET and auth', function(done){ assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body); // table should not get a cache channel as it won't get invalidated assert.ok(!res.headers.hasOwnProperty('x-cache-channel')); - assert.equal(res.headers['cache-control'], 'no-cache,max-age=300,must-revalidate,public'); + + const fallbackTtl = global.settings.cache.fallbackTtl || 300; + const cacheControl = res.headers['cache-control']; + const [ , maxAge ] = cacheControl.split(','); + const [ , value ] = maxAge.split('='); + + assert.ok(Number(value) <= fallbackTtl); + var pbody = JSON.parse(res.body); assert.equal(pbody.total_rows, 1); assert.equal(pbody.rows[0].count, 0); diff --git a/test/acceptance/cache-headers-test.js b/test/acceptance/cache-headers-test.js index 9a179a94..5371007a 100644 --- a/test/acceptance/cache-headers-test.js +++ b/test/acceptance/cache-headers-test.js @@ -55,7 +55,12 @@ describe('cache headers', function () { method: 'GET' }, {}, function(err, res) { - assert.equal(res.headers['cache-control'], `no-cache,max-age=${fallbackTtl},must-revalidate,public`); + const fallbackTtl = global.settings.cache.fallbackTtl || 300; + const cacheControl = res.headers['cache-control']; + const [ , maxAge ] = cacheControl.split(','); + const [ , value ] = maxAge.split('='); + + assert.ok(Number(value) <= fallbackTtl); assert.response(server, { url: `/api/v1/sql?${qs.encode({