From ca485f27adf6bd1eb4c38fe5be022ab0b9dbe782 Mon Sep 17 00:00:00 2001 From: Kornel Date: Sat, 21 Apr 2018 16:55:52 +0100 Subject: [PATCH] Option to usefully cheat on max-age calculation against the spec --- README.md | 3 +++ index.js | 12 ++++++++++-- test/misctest.js | 7 ++++--- test/responsetest.js | 23 +++++++++++++++++++++-- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9ecc598..731e540 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ const options = { cacheHeuristic: 0.1, immutableMinTimeToLive: 24*3600*1000, // 24h ignoreCargoCult: false, + trustServerDate: true, }; ``` @@ -72,6 +73,8 @@ If `options.shared` is `true` (default), then the response is evaluated from a p If `options.ignoreCargoCult` is true, common anti-cache directives will be completely ignored if the non-standard `pre-check` and `post-check` directives are present. These two useless directives are most commonly found in bad StackOverflow answers and PHP's "session limiter" defaults. +If `options.trustServerDate` is false, then server's `Date` header won't be used as the base for `max-age`. This is against the RFC, but it's useful if you want to cache responses with very short `max-age`, but your local clock is not exactly in sync with the server's. + ### `storable()` Returns `true` if the response can be stored in a cache. If it's `false` then you MUST NOT store either the request or the response. diff --git a/index.js b/index.js index 1d95f7c..c175a96 100644 --- a/index.js +++ b/index.js @@ -43,7 +43,7 @@ function formatCacheControl(cc) { } module.exports = class CachePolicy { - constructor(req, res, {shared, cacheHeuristic, immutableMinTimeToLive, ignoreCargoCult, _fromObject} = {}) { + constructor(req, res, {shared, cacheHeuristic, immutableMinTimeToLive, ignoreCargoCult, trustServerDate, _fromObject} = {}) { if (_fromObject) { this._fromObject(_fromObject); return; @@ -56,6 +56,7 @@ module.exports = class CachePolicy { this._responseTime = this.now(); this._isShared = shared !== false; + this._trustServerDate = undefined !== trustServerDate ? trustServerDate : true; this._cacheHeuristic = undefined !== cacheHeuristic ? cacheHeuristic : 0.1; // 10% matches IE this._immutableMinTtl = undefined !== immutableMinTimeToLive ? immutableMinTimeToLive : 24*3600*1000; @@ -241,6 +242,13 @@ module.exports = class CachePolicy { * @return timestamp */ date() { + if (this._trustServerDate) { + return this._serverDate(); + } + return this._responseTime; + } + + _serverDate() { const dateValue = Date.parse(this._resHeaders.date) if (isFinite(dateValue)) { const maxClockDrift = 8*3600*1000; @@ -313,7 +321,7 @@ module.exports = class CachePolicy { const defaultMinTtl = this._rescc.immutable ? this._immutableMinTtl : 0; - const dateValue = this.date(); + const dateValue = this._serverDate(); if (this._resHeaders.expires) { const expires = Date.parse(this._resHeaders.expires); // A cache recipient MUST interpret invalid date formats, especially the value "0", as representing a time in the past (i.e., "already expired"). diff --git a/test/misctest.js b/test/misctest.js index 485b34c..27b7bfb 100644 --- a/test/misctest.js +++ b/test/misctest.js @@ -31,11 +31,11 @@ describe('Other', function() { }); }); - it('GitHub response', function() { + it('GitHub response with small clock skew', function() { const res = { headers: { server: 'GitHub.com', - date: new Date().toUTCString(), + date: new Date(Date.now()-77*1000).toUTCString(), 'content-type': 'application/json; charset=utf-8', 'transfer-encoding': 'chunked', connection: 'close', @@ -69,7 +69,8 @@ describe('Other', function() { }; const c = new CachePolicy(req, res, { - shared: false + shared: false, + trustServerDate: false, }); assert(c.satisfiesWithoutRevalidation(req)); }) diff --git a/test/responsetest.js b/test/responsetest.js index 1634230..f15b2a6 100644 --- a/test/responsetest.js +++ b/test/responsetest.js @@ -87,14 +87,33 @@ describe('Response headers', function() { }); it('cache with expires', function() { + const now = Date.now(); const cache = new CachePolicy(req, {headers:{ - 'date': new Date().toGMTString(), - 'expires': new Date(Date.now() + 2000).toGMTString(), + 'date': new Date(now).toGMTString(), + 'expires': new Date(now + 2000).toGMTString(), }}); assert(!cache.stale()); assert.equal(2, cache.maxAge()); }); + it('cache with expires relative to date', function() { + const now = Date.now(); + const cache = new CachePolicy(req, {headers:{ + 'date': new Date(now-3000).toGMTString(), + 'expires': new Date(now).toGMTString(), + }}); + assert.equal(3, cache.maxAge()); + }); + + it('cache with expires always relative to date', function() { + const now = Date.now(); + const cache = new CachePolicy(req, {headers:{ + 'date': new Date(now-3000).toGMTString(), + 'expires': new Date(now).toGMTString(), + }},{trustServerDate:false}); + assert.equal(3, cache.maxAge()); + }); + it('cache expires no date', function() { const cache = new CachePolicy(req, {headers:{ 'cache-control': 'public',