1 | ;
|
2 |
|
3 | var onFinished = require('on-finished')
|
4 | , VError = require('verror')
|
5 | , replaceDateHeader = require('./replace-date-header')
|
6 | , getHttpResponseData = require('./parse-http-response');
|
7 |
|
8 | /**
|
9 | * Return an instance that can be used to write to a cache store/engine
|
10 | * @param {Object} opts
|
11 | * @param {Function} callback
|
12 | * @return {Object}
|
13 | */
|
14 | module.exports = function getExpeditiousCacheMiddleware (opts) {
|
15 |
|
16 | // Verify options are valid. Will throw an AssertionError if not
|
17 | require('./verify-options')(opts);
|
18 |
|
19 | // An expeditious instance that we will use for caching operations
|
20 | var cache = opts.expeditious;
|
21 |
|
22 | // Module that will be used to prevent us attempting to cache the same
|
23 | // request multiple times concurrently, thus reducing memory bloat and
|
24 | // unecessary cpu usgae
|
25 | var cacheLocks = require('./cache-locks')(opts);
|
26 |
|
27 | // Determines if caching should occur for a given request
|
28 | var shouldCache = require('./should-cache')(opts);
|
29 |
|
30 | // Generates the key used to cache a request
|
31 | var genCacheKey = require('./cache-key')(opts);
|
32 |
|
33 | // Determine the ttl for cache entries
|
34 | var getCacheExpiry = require('./cache-expiry')(opts);
|
35 |
|
36 |
|
37 | return function _expeditiousCacheMiddleware (req, res, next) {
|
38 | var log = require('./log')(req)
|
39 | , needsStorage = false
|
40 | , cacheKey = genCacheKey(req);
|
41 |
|
42 | log('checking cache for request using key "%s"', cacheKey);
|
43 |
|
44 | // Stop here if the request should not be cached. Think of the environment!
|
45 | if (!shouldCache(req)) {
|
46 | return next();
|
47 | }
|
48 |
|
49 | // This request could already be cached, so let's start by checking that
|
50 | cache.get({
|
51 | key: cacheKey
|
52 | }, onCacheReturned);
|
53 |
|
54 |
|
55 | /**
|
56 | * Callback for when we load a value from cache, decides if we proceed with
|
57 | * the request or send back the cached data (if it existed)
|
58 | * @param {Error} err
|
59 | * @param {String} value
|
60 | */
|
61 | function onCacheReturned (err, httpData) {
|
62 | log('loaded data from cache:\n%s', JSON.stringify(httpData, null, 2));
|
63 |
|
64 | if (err) {
|
65 | log('cache error, proceed with request');
|
66 | proceedWithRequest();
|
67 | } else if (httpData) {
|
68 | log('cache hit');
|
69 | respondWithCache(httpData);
|
70 | } else {
|
71 | log('cache miss, proceed with request');
|
72 | proceedWithRequest();
|
73 | }
|
74 | }
|
75 |
|
76 | /**
|
77 | * Respond to a request with a value that the cache returned
|
78 | * @param {Object} httpData
|
79 | */
|
80 | function respondWithCache (httpData) {
|
81 | log('responding with cached value');
|
82 |
|
83 | res.set('etag', httpData.etag);
|
84 |
|
85 | if (req.fresh) {
|
86 | log('returning a 304, etag matches');
|
87 | // req.fresh is an express "defineProperty" added that will perform
|
88 | // a check that the incoming etag matches the one on the res headers
|
89 | res.status(304);
|
90 | res.end();
|
91 | } else {
|
92 | log('returning cached data, etags did not match');
|
93 | // We're writing a HTTP response body directly to the socket, therefore
|
94 | // we use socket.write instead of .send or others to prevent adding
|
95 | // unwanted headers and response data
|
96 | res.socket.write(replaceDateHeader(httpData.completeHttpBody));
|
97 | res.end();
|
98 | }
|
99 | }
|
100 |
|
101 | /**
|
102 | * Called once we finish writing data to the client.
|
103 | * Writes the returned data to the cache if no errors occurred.
|
104 | * @param {Error} err
|
105 | */
|
106 | function onResponseFinished (err, httpData) {
|
107 | /* istanbul ignore else */
|
108 | if (err) {
|
109 | log('error processing request, not storing response in cache');
|
110 |
|
111 | cacheLocks.removeLock(cacheKey);
|
112 | } else if (needsStorage) {
|
113 |
|
114 | // If the cache time is 0 don't bother doing a write
|
115 | if (getCacheExpiry(res.statusCode) === 0) {
|
116 | log('cache time for %s is 0 - will not be cached', res.statusCode);
|
117 | cacheLocks.removeLock(cacheKey);
|
118 | } else {
|
119 | log(
|
120 | 'writing response to storage with key "%s", response:\n%s',
|
121 | cacheKey,
|
122 | JSON.stringify(httpData, null, 2)
|
123 | );
|
124 |
|
125 | cache.set({
|
126 | key: cacheKey,
|
127 | val: httpData,
|
128 | ttl: getCacheExpiry(res.statusCode)
|
129 | }, function (err) {
|
130 | if (err) {
|
131 | log('failed to write cache');
|
132 | log(err);
|
133 | } else {
|
134 | log(
|
135 | 'wrote response to storage with key "%s"',
|
136 | req.originalUrl,
|
137 | cacheKey
|
138 | );
|
139 | }
|
140 |
|
141 | cacheLocks.removeLock(cacheKey);
|
142 | });
|
143 | }
|
144 |
|
145 | }
|
146 | }
|
147 |
|
148 |
|
149 | /**
|
150 | * Processes this request using the handler the express application has
|
151 | * defined, but will cache the response from that handler
|
152 | * @return {undefined}
|
153 | */
|
154 | function proceedWithRequest () {
|
155 | // Hacky, but so is overwriting res.end, res.send, etc. By overwriting
|
156 | // res.socket.write we simplify our job since we can cache the "raw" http
|
157 | // response and don't need to rebuild it for future requests
|
158 | res.socket.write = (function (res) {
|
159 | var buf = ''
|
160 | , isInitialWrite = true
|
161 | , _write = res.socket.write.bind(res.socket);
|
162 |
|
163 | // We listen for the request to emit "end" or "finish" and then write to
|
164 | // the cache since we have everything we need to do so
|
165 | onFinished(res, function (err) {
|
166 | /* istanbul ignore else */
|
167 | if (err) {
|
168 | log('request finished with error, not parsing response for cache');
|
169 | onResponseFinished(err);
|
170 | } else if (needsStorage) {
|
171 | log('request finished, parsing response for cache');
|
172 | var res = null;
|
173 |
|
174 | try {
|
175 | res = getHttpResponseData(new Buffer(buf));
|
176 | } catch (e) {
|
177 | err = new VError(e, 'failed to parse http response');
|
178 | }
|
179 |
|
180 | onResponseFinished(err, res);
|
181 | } else {
|
182 | log(
|
183 | 'request didn\'t need to be cached, another request for this ' +
|
184 | 'resource was cached first'
|
185 | );
|
186 | }
|
187 | });
|
188 |
|
189 | return function customWrite (body) {
|
190 | // On the initial write for this response we need to check if a
|
191 | // matching request is already being written to the cache, and if it
|
192 | // is then we won't attempt to cache this since it would have a large
|
193 | // performance penalty in high concurrency environments
|
194 | /* istanbul ignore else */
|
195 | if (isInitialWrite) {
|
196 | log(
|
197 | 'initial write called for request %s determining if caching ' +
|
198 | 'is required',
|
199 | req.originalUrl
|
200 | );
|
201 |
|
202 | isInitialWrite = false;
|
203 |
|
204 | // If a lock does not already exist we need to cache this response
|
205 | needsStorage = !cacheLocks.isLocked(cacheKey);
|
206 |
|
207 | /* istanbul ignore else */
|
208 | if (needsStorage) {
|
209 | log('caching entry needs to be created for this request');
|
210 | cacheLocks.addLock(cacheKey);
|
211 | }
|
212 | }
|
213 |
|
214 | /* istanbul ignore else */
|
215 | if (needsStorage) {
|
216 | // Build this cache entry so it can be written to the cache
|
217 | buf += body.toString();
|
218 | }
|
219 |
|
220 | // We still need to write to the original socket.write function
|
221 | _write.apply(_write, Array.prototype.slice.call(arguments));
|
222 | };
|
223 | })(res);
|
224 |
|
225 | // Send the request to the next function in the routing stack, our custom
|
226 | // res.socket.write above will be invoked as soon as the application
|
227 | // starts writing a response
|
228 | next();
|
229 | }
|
230 | };
|
231 | };
|