Skip to content

Commit

Permalink
Merge pull request #6 from finanzcheck/feature/CORE-440-respect_respo…
Browse files Browse the repository at this point in the history
…nse_cache_headers

Respect Server response Cache related headers.
  • Loading branch information
jpodwys authored Feb 5, 2020
2 parents 02e41c5 + c4b5b14 commit 7680e43
Show file tree
Hide file tree
Showing 5 changed files with 572 additions and 99 deletions.
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Require and instantiate superagent-cache-plugin as follows to get the [default c
```javascript
// Require and instantiate a cache module
var cacheModule = require('cache-service-cache-module');
var cache = new cacheModule({storage: 'session', defaultExpiration: 60});
var cache = new cacheModule({storage: 'session'});

// Require superagent-cache-plugin and pass your cache module
var superagentCache = require('superagent-cache-plugin')(cache);
Expand Down Expand Up @@ -100,6 +100,23 @@ All options that can be passed to the `defaults` `require` param can be overwrit
* cacheWhenEmpty
* doQuery
* forceUpdate
* bypassHeaders

# The `Cache-Control` request/response headers related behavior

* Setting the request header `Cache-Control: maxe-age=X` is an alternative to the `.expiration(X)` API method call.
* Setting the request header `Cache-Control: only-if-cached` is an alternative to the `.doQuery(false)` API method call.
* Setting the `Cache-Control`request header value to one of `maxe-age=0`, `no-cache`, `no-store`switches of caching of the response.

**NOTE** The plugin respects the server response cache related headers (`Cache-Control`, `Pragma: no-cache`, `Expires`) and calculates proper TTL for cached responses, with the respect to following:

* When the `expiration` option is unspecified, the default behavior will be no caching, unless the server response `Cache-Control` header specifies otherwise.
* The `expiration=0` specified in options will switch off caching for any request, even when the server specifies that the response is cacheable and provides non-zero TTL via eg. the `Cache-Control: max-age=X` header.
* The non-zero `expiration` option value will narrow down any of TTL value specified via server response `Cache-Control` header.
* When the `expiration` option value is greater than the TTL specified via server response `Cache-Control` header, the later wins.

## `ETag` and `Last-Modified` support.
The `ETag` and `Last-Modified` related cache behavior is supported with sending the associated request headers for cached response revalidation and proper handling of the `304 Not Modified` response, which results in serving the cached response instead `304` one.

# Supported Caches

Expand Down Expand Up @@ -303,6 +320,43 @@ Tells `superagent-cache-plugin` to perform an ajax call regardless of whether th

* bool: boolean, default: false

## .bypassHeaders(headerNames)

Tells `superagent-cache-plugin` to copy given headers from the current executing request to the response.
This is useful for eg. some correlation ID, which binds a request with a response and could be an issue when returning a cached response.
**Note** Bypassed headers are copied only to cached responses.

#### Arguments

* headerNames: string or array of strings

#### Example

```javascript
//the superagent query will be executed with all headers
//but the key used to store the superagent response will be generated without the 'bypassHeaders' header keys
//and the response will have those keys set to the values from the request headers, when served from a cache.
var correlationId = 0;
superagent
.get(uri)
.use(superagentCache)
.expiration(1)
.bypassHeaders(['x-correlation-id'])
.set('x-correlation-id', correlationId++)
.end(function (error, response){
superagent
.get(uri)
.use(superagentCache)
.bypassHeaders(['x-correlation-id'])
.set('x-correlation-id', correlationId++)
.end(function (error, response){
expect(response.header['x-cache']).toBe('HIT');
expect(response.header['x-correlation-id']).toBe(1);
});
}
);
```

## superagentCache.cache

This is the first constructor param you handed in when you instantiated `superagent-cache-plugin`.
Expand Down
104 changes: 88 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const CachePolicy = require('http-cache-semantics');
var utils = require('./utils');

/**
Expand Down Expand Up @@ -67,7 +68,7 @@ module.exports = function(cache, defaults){
*/
Request.expiration = function(expiration){
props.expiration = expiration;
return Request;
return Request.set('cache-control', 'max-age=' + expiration);
}

/**
Expand All @@ -89,7 +90,44 @@ module.exports = function(cache, defaults){
}

/**
* Save the exisitng .end() value ("namespaced" in case of other plugins)
* Array of header names, which should be bypassed from a request to a response.
* This is useful for eg. some correlation ID, which binds a request with a response
* and could be an issue when returning the cached response.
* Note, that bypassed headers are copied only to cached responses.
*
* @param {string|string[]} bypassHeaders
*/
Request.bypassHeaders = function(bypassHeaders){
props.bypassHeaders = (typeof bypassHeaders === 'string') ? [bypassHeaders] : bypassHeaders;
return Request;
}

var cachedEntry;

// Special handling for the '304 Not Modified' case, which will only come out
// in case of server responses with 'ETag' and/or 'Last-Modified' headers.
Request.on('response', function (res) {
if (res.status === 304 && cachedEntry) {
// update the cache entry
const key = utils.keygen(Request, props);
const policy = CachePolicy.fromObject(cachedEntry.policy).revalidatedPolicy(Request.toJSON(), res).policy;
cachedEntry.policy = policy.toObject();
cache.set(key, cachedEntry, utils.getExpiration(props, policy));
// modify response
res.status = cachedEntry.response.status;
res.statusCode = cachedEntry.response.statusCode;
res.header = policy.responseHeaders();
utils.setResponseHeader(res, 'x-cache', 'HIT');
utils.copyBypassHeaders(res, Request, props);
res.body = cachedEntry.response.body;
res.text = cachedEntry.response.text;
// cleanup
cachedEntry = undefined;
}
});

/**
* Save the existing .end() value ("namespaced" in case of other plugins)
* so that we can provide our customized .end() and then call through to
* the underlying implementation.
*/
Expand All @@ -100,46 +138,81 @@ module.exports = function(cache, defaults){
* @param {function} cb
*/
Request.end = function(cb){
utils.handleReqCacheHeaders(Request, props);
Request.scRedirectsList = Request.scRedirectsList || [];
Request.scRedirectsList = Request.scRedirectsList.concat(Request._redirectList);
if(~supportedMethods.indexOf(Request.method.toUpperCase())){
var _Request = Request;
var key = utils.keygen(Request, props);
if(~cacheableMethods.indexOf(Request.method.toUpperCase())){
cache.get(key, function (err, response){
if(!err && response && !props.forceUpdate){
utils.callbackExecutor(cb, err, response, key);
cache.get(key, function (err, entry) {
cachedEntry = entry;
const cachedResponse = entry ? entry.response : undefined;
var policy = entry && entry.policy ? CachePolicy.fromObject(entry.policy) : undefined;
if (cachedResponse && policy) {
cachedResponse.header = policy.responseHeaders();
utils.setResponseHeader(cachedResponse, 'x-cache', 'HIT');
utils.copyBypassHeaders(cachedResponse, Request, props);
}
if(!err && cachedResponse && policy
&& policy.satisfiesWithoutRevalidation(Request.toJSON()) && !props.forceUpdate) {
// Return the clone of the cached response.
return utils.callbackExecutor(cb, null, JSON.parse(JSON.stringify(cachedResponse)), key, Request);
}
else{
if(props.doQuery){
if (policy) {
const headers = policy.revalidationHeaders(Request.toJSON());
Object.keys(headers).forEach(function(key) {
Request = Request.set(key, headers[key]);
});
}
end.call(Request, function (err, response){
if(err){
return utils.callbackExecutor(cb, err, response, key);
}
else if(!err && response){
else if(response){
response.redirects = _Request.scRedirectsList;
policy = new CachePolicy(Request.toJSON(), utils.gutResponse(response, Request));
if(props.prune){
response = props.prune(response, utils.gutResponse);
}
else if(props.responseProp){
else if(props.responseProp) {
response = response[props.responseProp] || null;
}
else{
response = utils.gutResponse(response);
response = utils.gutResponse(response, Request);
}
if(!utils.isEmpty(response) || props.cacheWhenEmpty){
cache.set(key, response, props.expiration, function (){
return utils.callbackExecutor(cb, err, response, key);
});
utils.setResponseHeader(response, 'x-cache', 'MISS');
if ((0 !== props.expiration) && (!utils.isEmpty(response) || props.cacheWhenEmpty)) {
if (policy.storable() && policy.timeToLive() > 0) {
// The TTL in underlying caches will be policy TTL x 2, as we want to allow for
// further serving of the stale objects (when the policy allows for that).
const expiration = utils.getExpiration(props, policy);
const entry = { policy: policy.toObject() , response: response };
cache.set(key, entry, expiration , function () {
return utils.callbackExecutor(cb, null, response, key);
});
}
else {
return utils.callbackExecutor(cb, null, response, key);
}
}
else{
return utils.callbackExecutor(cb, err, response, key);
return utils.callbackExecutor(cb, null, response, key);
}
}
});
}
else{
return utils.callbackExecutor(cb, null, null, key);
// This is actually the 'only-if-cached' condition
// (doQuery=false is exactly the same intention).
// Returning the response status 504 as the RFC2616 states about the 'only-if-cached'.
// See: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4.
return utils.callbackExecutor(cb, null, {
status: 504,
header: {},
}, key);
}
}
});
Expand All @@ -165,7 +238,6 @@ module.exports = function(cache, defaults){
});
}
}

return Request;
return props.expiration !== undefined ? Request.set('Cache-control', 'max-age=' + props.expiration) : Request;
}
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,8 @@
"plugin",
"browser",
"node"
]
],
"dependencies": {
"http-cache-semantics": "~3.7.3"
}
}
Loading

0 comments on commit 7680e43

Please sign in to comment.