diff --git a/index.js b/index.js index 669eabe..ef41507 100644 --- a/index.js +++ b/index.js @@ -189,15 +189,15 @@ module.exports = class CachePolicy { return true; } - responseHeaders() { + _copyWithoutHopByHopHeaders(inHeaders) { const headers = {}; - for(const name in this._resHeaders) { + for(const name in inHeaders) { if (hopByHopHeaders[name]) continue; - headers[name] = this._resHeaders[name]; + headers[name] = inHeaders[name]; } // 9.1. Connection - if (this._resHeaders.connection) { - const tokens = this._resHeaders.connection.trim().split(/\s*,\s*/); + if (inHeaders.connection) { + const tokens = inHeaders.connection.trim().split(/\s*,\s*/); for(const name of tokens) { delete headers[name]; } @@ -212,6 +212,11 @@ module.exports = class CachePolicy { headers.warning = warnings.join(',').trim(); } } + return headers; + } + + responseHeaders() { + const headers = this._copyWithoutHopByHopHeaders(this._resHeaders); headers.age = `${Math.round(this.age())}`; return headers; } @@ -348,25 +353,32 @@ module.exports = class CachePolicy { }; } - revalidationHeaders(incoming_req) { - this._assertRequestHasHeaders(incoming_req); - if (!this._resHeaders.etag && !this._resHeaders['last-modified']) { - return incoming_req.headers; // no validators available - } - // revalidation allowed via HEAD - if (!this._requestMatches(incoming_req, true)) { - return incoming_req.headers; // not for the same resource + /** + * Headers for sending to the origin server to revalidate stale response. + * Allows server to return 304 to allow reuse of the previous response. + * + * Hop by hop headers are always stripped. + * Revalidation headers may be added or removed, depending on request. + */ + revalidationHeaders(incomingReq) { + this._assertRequestHasHeaders(incomingReq); + const headers = this._copyWithoutHopByHopHeaders(incomingReq.headers); + + if (!this._requestMatches(incomingReq, true) || !this.storable()) { // revalidation allowed via HEAD + // not for the same resource, or wasn't allowed to be cached anyway + delete headers['if-none-match']; + delete headers['if-modified-since']; + return headers; } - const headers = Object.assign({}, incoming_req.headers); - /* MUST send that entity-tag in any cache validation request (using If-Match or If-None-Match) if an entity-tag has been provided by the origin server. */ if (this._resHeaders.etag) { - headers['if-none-match'] = this._resHeaders.etag; + headers['if-none-match'] = headers['if-none-match'] ? `${headers['if-none-match']}, ${this._resHeaders.etag}` : this._resHeaders.etag; } + /* SHOULD send the Last-Modified value in non-subrange cache validation requests (using If-Modified-Since) if only a Last-Modified value has been provided by the origin server. Note: This implementation does not understand partial responses (206) */ - if (this._resHeaders['last-modified'] && this.storable()) { + if (this._resHeaders['last-modified'] && !headers['if-modified-since']) { headers['if-modified-since'] = this._resHeaders['last-modified']; } diff --git a/package.json b/package.json index 53a7e45..9602f6c 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,16 @@ { "name": "http-cache-semantics", - "version": "3.5.0", + "version": "3.5.1", "description": "Parses Cache-Control and other headers. Helps building correct HTTP caches and proxies", "main": "index.js", "repository": "https://github.com/pornel/http-cache-semantics.git", "scripts": { "test": "mocha" }, + "files": [ + "index.js", + "test" + ], "author": "Kornel LesiƄski (https://kornel.ski/)", "license": "BSD-2-Clause", "devDependencies": { diff --git a/test/revalidatetest.js b/test/revalidatetest.js index 6f5d531..12e9ef5 100644 --- a/test/revalidatetest.js +++ b/test/revalidatetest.js @@ -5,7 +5,11 @@ const CachePolicy = require('..'); const simpleRequest = { method:'GET', - headers:{host:'www.w3c.org'}, + headers:{ + host:'www.w3c.org', + connection: 'close', + 'x-custom': 'yes', + }, url:'/Protocols/rfc2616/rfc2616-sec14.html', }; function simpleRequestBut(overrides) { @@ -18,57 +22,88 @@ const lastModifiedResponse = {headers:Object.assign({'last-modified':'Tue, 15 No const multiValidatorResponse = {headers:Object.assign({},etaggedResponse.headers,lastModifiedResponse.headers)}; const alwaysVariableResponse = {headers:Object.assign({'vary':'*'},cacheableResponse.headers)}; +function assertHeadersPassed(headers) { + assert.strictEqual(headers.connection, undefined); + assert.strictEqual(headers['x-custom'], 'yes'); +} +function assertNoValidators(headers) { + assert.strictEqual(headers['if-none-match'], undefined); + assert.strictEqual(headers['if-modified-since'], undefined); +} + describe('Can be revalidated?', function() { it('ok if method changes to HEAD', function(){ - const cache = new CachePolicy(simpleRequest,etaggedResponse); - const headers = cache.revalidationHeaders(simpleRequestBut({method:'HEAD'})); - assert.equal(headers['if-none-match'], '"123456789"'); + const cache = new CachePolicy(simpleRequest,etaggedResponse); + const headers = cache.revalidationHeaders(simpleRequestBut({method:'HEAD'})); + assertHeadersPassed(headers); + assert.equal(headers['if-none-match'], '"123456789"'); }); - it('not if method mismatch (other than HEAD)',function(){ - const cache = new CachePolicy(simpleRequest,etaggedResponse); - const incomingRequest = simpleRequestBut({method:'POST'}); - // Returns the same object unmodified, which means no custom validation - assert.strictEqual(incomingRequest.headers, cache.revalidationHeaders(incomingRequest)); + + it('not if method mismatch (other than HEAD)', function(){ + const cache = new CachePolicy(simpleRequest,etaggedResponse); + const incomingRequest = simpleRequestBut({method:'POST'}); + // Returns the same object unmodified, which means no custom validation + const headers = cache.revalidationHeaders(incomingRequest); + assertHeadersPassed(headers); + assertNoValidators(headers); }); - it('not if url mismatch',function(){ - const cache = new CachePolicy(simpleRequest,etaggedResponse); - const incomingRequest = simpleRequestBut({url:'/yomomma'}); - assert.strictEqual(incomingRequest.headers, cache.revalidationHeaders(incomingRequest)); + + it('not if url mismatch', function(){ + const cache = new CachePolicy(simpleRequest,etaggedResponse); + const incomingRequest = simpleRequestBut({url:'/yomomma'}); + const headers = cache.revalidationHeaders(incomingRequest); + assertHeadersPassed(headers); + assertNoValidators(headers); }); - it('not if host mismatch',function(){ + + it('not if host mismatch', function(){ const cache = new CachePolicy(simpleRequest,etaggedResponse); const incomingRequest = simpleRequestBut({headers:{host:'www.w4c.org'}}); - assert.strictEqual(incomingRequest.headers, cache.revalidationHeaders(incomingRequest)); + const headers = cache.revalidationHeaders(incomingRequest); + assertNoValidators(headers); + assert.strictEqual(headers['x-custom'], undefined); }); - it('not if vary fields prevent',function(){ - const cache = new CachePolicy(simpleRequest,alwaysVariableResponse); - assert.strictEqual(simpleRequest.headers, cache.revalidationHeaders(simpleRequest)); + + it('not if vary fields prevent', function(){ + const cache = new CachePolicy(simpleRequest,alwaysVariableResponse); + const headers = cache.revalidationHeaders(simpleRequest); + assertHeadersPassed(headers); + assertNoValidators(headers); }); + it('when entity tag validator is present', function() { const cache = new CachePolicy(simpleRequest, etaggedResponse); const headers = cache.revalidationHeaders(simpleRequest); + assertHeadersPassed(headers); assert.equal(headers['if-none-match'], '"123456789"'); }); + it('when last-modified validator is present', function() { - const cache = new CachePolicy(simpleRequest, lastModifiedResponse); - const headers = cache.revalidationHeaders(simpleRequest); - assert.equal(headers['if-modified-since'], 'Tue, 15 Nov 1994 12:45:26 GMT'); + const cache = new CachePolicy(simpleRequest, lastModifiedResponse); + const headers = cache.revalidationHeaders(simpleRequest); + assertHeadersPassed(headers); + assert.equal(headers['if-modified-since'], 'Tue, 15 Nov 1994 12:45:26 GMT'); }); + it('not without validators', function() { const cache = new CachePolicy(simpleRequest, cacheableResponse); - assert.strictEqual(simpleRequest.headers, cache.revalidationHeaders(simpleRequest)); + const headers = cache.revalidationHeaders(simpleRequest); + assertHeadersPassed(headers); + assertNoValidators(headers); }) }); describe('Validation request', function(){ + it('must contain any etag', function(){ const cache = new CachePolicy(simpleRequest,multiValidatorResponse); const expected = multiValidatorResponse.headers.etag; const actual = cache.revalidationHeaders(simpleRequest)['if-none-match']; assert.equal(actual,expected); }); - it('should send the Last-Modified value',function(){ + + it('should send the Last-Modified value', function(){ const cache = new CachePolicy(simpleRequest,multiValidatorResponse); const expected = multiValidatorResponse.headers['last-modified']; const actual = cache.revalidationHeaders(simpleRequest)['if-modified-since'];