Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cache option #284

Merged
merged 80 commits into from
Oct 15, 2017
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
43023d5
Optional response caching
lukechilds Mar 22, 2017
49b6f00
Return cached response immediately if not stale
lukechilds Mar 22, 2017
2811293
Add basic cache tests
lukechilds May 4, 2017
0f6f944
Drop expired for http-cache-semantics
lukechilds May 7, 2017
0d6180a
If we retrieve a stale cache entry, delete it immediately
lukechilds May 7, 2017
011ea0e
Include method in cache key
lukechilds May 8, 2017
5ec945f
Allow opts.cache.get to return value or Promise
lukechilds May 9, 2017
817de11
opts.cache API should match Map() API
lukechilds May 9, 2017
b3201ea
Use Map() as cache store in cache tests
lukechilds May 9, 2017
b65128e
Move cache read logic into separate function
lukechilds May 9, 2017
b0490ca
Simplify cache option check
lukechilds May 9, 2017
903fbfa
Store status code and url in cache. Don't store headers.
lukechilds May 10, 2017
b8b3411
Don't store stream in unnecessary variable
lukechilds May 10, 2017
39811aa
Object destructuring on single line
lukechilds May 10, 2017
e4ae2c6
Don't serialize cache object to JSON
lukechilds May 11, 2017
f64dcc7
Move server endpoints inside test function for readability
lukechilds May 11, 2017
a31c966
Store body data encoding so we can reliably convert to buffer
lukechilds May 11, 2017
4e98913
Test binary responses are cached
lukechilds May 11, 2017
f2026dc
Test cached response is re-encoded to current encoding option
lukechilds May 11, 2017
b3f23ea
Move test server endpoints out into test.before
lukechilds May 11, 2017
7d4365e
Check in binary test that data actually is binary
lukechilds May 11, 2017
70d57d7
Test stream responses are cached
lukechilds May 16, 2017
838821d
Rafactor caching into external function and only read stream once
lukechilds May 16, 2017
dbb7f20
Cache stream responses
lukechilds May 16, 2017
d4cf24c
Don't save buffer encoding, it's not needed
lukechilds May 16, 2017
159c399
Use pipe instead of manually piping inside data
lukechilds May 16, 2017
23d76e3
Don't redo what getStream already does
lukechilds May 16, 2017
bd99d4d
requests => responses
lukechilds May 17, 2017
be4c431
Make sure "/no-cache" test endpoint is completely unstorable
lukechilds May 17, 2017
58ffa2b
Check the cache is actually being filled
lukechilds May 17, 2017
6212f16
Move caching logic back into get()
lukechilds May 17, 2017
75a9995
Include stream processing inside cache function
lukechilds May 17, 2017
f6f915b
No point doing caching inside a function now it's only used once
lukechilds May 17, 2017
8ee8a4a
More accurate test
lukechilds May 17, 2017
4e3a3dc
Rename /no-cache test endpoint to /no-store
lukechilds May 18, 2017
b6b3401
Test stale cache entries with Last-Modified headers are revalidated
lukechilds May 18, 2017
f56c508
Test stale cache entries with ETag headers are revalidated
lukechilds May 18, 2017
07a403c
Revalidate stale cached responses that have Last-Modified/ETag headers.
lukechilds May 18, 2017
b63a641
Delete stale cache entries that can't be revalidated
lukechilds May 19, 2017
459cbbf
swap no-cache for max-age=0
lukechilds May 19, 2017
a67f157
Test stale cache entries that can't be revalidate are deleted from cache
lukechilds May 19, 2017
1843cba
Pass TTL to cache
lukechilds May 23, 2017
c2c920f
Test TTL is passed to cache
lukechilds May 23, 2017
e328719
Add `fromCache` property to response object
lukechilds May 31, 2017
0ef2c4f
Test response objects have fromCache property set correctly
lukechilds May 31, 2017
81e5b98
Remove package-lock.json
lukechilds Jun 10, 2017
8f4b02b
Extract caching logic into cacheable-request module
lukechilds Jun 10, 2017
7148409
Wrap all requests in cacheable-request for easier handling of native …
lukechilds Jun 10, 2017
6814128
Remove unnecessary tests
lukechilds Jul 5, 2017
dbf96c6
Use Number() instead of parseInt
lukechilds Jul 5, 2017
b0c6fa8
Test redirects are cached and re-used internally
lukechilds Jul 5, 2017
4355e59
Increment test callback indexes at end of request
lukechilds Jul 5, 2017
cf95ab9
Merge branch 'master' into cache
lukechilds Aug 13, 2017
d45ac92
Temporarily skip cache tests
lukechilds Aug 13, 2017
8bf25ab
Wrap requests with cacheable-request
lukechilds Aug 13, 2017
f00749a
Don't skip cache tests
lukechilds Aug 13, 2017
be12b61
Pass cache option through as storage adadpter
lukechilds Aug 13, 2017
2616d53
Set cache as false by default
lukechilds Aug 13, 2017
13a9e1b
Fix merge conflicts 🎉
lukechilds Aug 13, 2017
ad9403a
Emit cache errors
lukechilds Aug 14, 2017
44a51c9
Test cache error throws got.CacheError
lukechilds Aug 14, 2017
eef529e
Add cache section to readme
lukechilds Aug 15, 2017
a36ab21
Add caching to highlights list
lukechilds Aug 15, 2017
56bf35f
Simplify Promise example
lukechilds Aug 15, 2017
f6541ed
Fix typo
lukechilds Aug 15, 2017
ac48967
Use async/await in example code
lukechilds Aug 17, 2017
b7fc611
No space in object brackets in example
lukechilds Aug 17, 2017
2b7176a
Document #### got.CacheError
lukechilds Aug 17, 2017
f182b5c
Document opts.cache
lukechilds Aug 17, 2017
b0d5704
Fix comment alignment
lukechilds Oct 3, 2017
dfd072b
object => Object
lukechilds Oct 3, 2017
8893e2c
Link to cache adapter section in cache API doc
lukechilds Oct 3, 2017
fe116bc
Fix cache href
lukechilds Oct 3, 2017
7f63136
More info on when a cache method will fail
lukechilds Oct 3, 2017
a3dd216
--save is no longer needed
lukechilds Oct 3, 2017
ff7c57d
Don't use e.g
lukechilds Oct 3, 2017
4d9ab59
Document new returned properties
lukechilds Oct 3, 2017
0a75785
Reword returned properties docs
lukechilds Oct 3, 2017
f249038
Fix duplicate link
lukechilds Oct 3, 2017
c27f7f5
Update to cacheable-request@2.0.0
lukechilds Oct 3, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const unzipResponse = require('unzip-response');
const createErrorClass = require('create-error-class');
const isRetryAllowed = require('is-retry-allowed');
const Buffer = require('safe-buffer').Buffer;
const cacheableRequest = require('cacheable-request');
const pkg = require('./package');

function requestAsEventEmitter(opts) {
Expand All @@ -30,7 +31,7 @@ function requestAsEventEmitter(opts) {
const get = opts => {
const fn = opts.protocol === 'https:' ? https : http;

const req = fn.request(opts, res => {
const cacheReq = cacheableRequest(fn.request, opts, res => {
const statusCode = res.statusCode;

if (isRedirect(statusCode) && opts.followRedirect && 'location' in res.headers && (opts.method === 'GET' || opts.method === 'HEAD')) {
Expand All @@ -54,30 +55,30 @@ function requestAsEventEmitter(opts) {
}

setImmediate(() => {
const response = typeof unzipResponse === 'function' && req.method !== 'HEAD' ? unzipResponse(res) : res;
const response = typeof unzipResponse === 'function' && opts.method !== 'HEAD' ? unzipResponse(res) : res;
response.url = redirectUrl || requestUrl;
response.requestUrl = requestUrl;

ee.emit('response', response);
});
});

req.once('error', err => {
const backoff = opts.retries(++retryCount, err);
cacheReq.on('request', req => {
req.once('error', err => {
const backoff = opts.retries(++retryCount, err);

if (backoff) {
setTimeout(get, backoff, opts);
return;
}
if (backoff) {
setTimeout(get, backoff, opts);
return;
}

ee.emit('error', new got.RequestError(err, opts));
});
ee.emit('error', new got.RequestError(err, opts));
});

if (opts.gotTimeout) {
timedOut(req, opts.gotTimeout);
}
if (opts.gotTimeout) {
timedOut(req, opts.gotTimeout);
}

setImmediate(() => {
ee.emit('request', req);
});
};
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"fetch"
],
"dependencies": {
"cacheable-request": "^0.3.0",
"create-error-class": "^3.0.0",
"duplexer3": "^0.1.4",
"get-stream": "^3.0.0",
Expand All @@ -59,6 +60,7 @@
"coveralls": "^2.11.4",
"form-data": "^2.1.1",
"get-port": "^3.0.0",
"get-stream": "^3.0.0",
"into-stream": "^3.0.0",
"nyc": "^10.0.0",
"pem": "^1.4.4",
Expand Down
197 changes: 197 additions & 0 deletions test/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import test from 'ava';
import getStream from 'get-stream';
import got from '../';
import {createServer} from './helpers/server';

let s;

test.before('setup', async () => {
s = await createServer();

let noStoreIndex = 0;
s.on('/no-store', (req, res) => {
noStoreIndex++;
res.setHeader('Cache-Control', 'public, no-cache, no-store');
res.end(noStoreIndex.toString());
});

let cacheIndex = 0;
s.on('/cache', (req, res) => {
cacheIndex++;
res.setHeader('Cache-Control', 'public, max-age=60');
res.end(cacheIndex.toString());
});

s.on('/last-modified', (req, res) => {
res.setHeader('Cache-Control', 'public, max-age=0');
res.setHeader('Last-Modified', 'Wed, 21 Oct 2015 07:28:00 GMT');
let responseBody = 'last-modified';

if (req.headers['if-modified-since'] === 'Wed, 21 Oct 2015 07:28:00 GMT') {
res.statusCode = 304;
responseBody = null;
}

res.end(responseBody);
});

s.on('/etag', (req, res) => {
res.setHeader('Cache-Control', 'public, max-age=0');
res.setHeader('ETag', '33a64df551425fcc55e4d42a148795d9f25f89d4');
let responseBody = 'etag';

if (req.headers['if-none-match'] === '33a64df551425fcc55e4d42a148795d9f25f89d4') {
res.statusCode = 304;
responseBody = null;
}

res.end(responseBody);
});

let cacheThenNoStoreIndex = 0;
s.on('/cache-then-no-store-on-revalidate', (req, res) => {
const cc = cacheThenNoStoreIndex === 0 ? 'public, max-age=0' : 'public, no-cache, no-store';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rename this to cacheControl.

cacheThenNoStoreIndex++;
res.setHeader('Cache-Control', cc);
res.end('cache-then-no-store-on-revalidate');
});

await s.listen(s.port);
});

test('Non cacheable responses are not cached', async t => {
const endpoint = '/no-store';
const cache = new Map();

const firstResponseInt = parseInt((await got(s.url + endpoint, {cache})).body, 10);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think xo had a rule favoring Number(x) over parseInt(x, 10).

const secondResponseInt = parseInt((await got(s.url + endpoint, {cache})).body, 10);

t.is(cache.size, 0);
t.true(firstResponseInt < secondResponseInt);
});

test('Cacheable responses are cached', async t => {
const endpoint = '/cache';
const cache = new Map();

const firstResponse = await got(s.url + endpoint, {cache});
const secondResponse = await got(s.url + endpoint, {cache});

t.is(cache.size, 1);
t.is(firstResponse.body, secondResponse.body);
});

test('Stream responses are cached', async t => {
const endpoint = '/cache';
const cache = new Map();

const firstResponseBody = await getStream(got.stream(s.url + endpoint, {cache}));
const secondResponseBody = await getStream(got.stream(s.url + endpoint, {cache}));

t.is(cache.size, 1);
t.is(firstResponseBody, secondResponseBody);
});

test('Binary responses are cached', async t => {
const endpoint = '/cache';
const cache = new Map();
const encoding = null;

const firstResponse = await got(s.url + endpoint, {cache, encoding});
const secondResponse = await got(s.url + endpoint, {cache, encoding});

t.is(cache.size, 1);
t.true(firstResponse.body instanceof Buffer);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

=> Buffer.isBuffer(firstResponse.body)

t.is(firstResponse.body.toString(), secondResponse.body.toString());
});

test('TTL is passed to cache', async t => {
const endpoint = '/cache';
const store = new Map();
const cache = {
get: store.get.bind(store),
set: (key, val, ttl) => {
t.true(typeof ttl === 'number');
t.true(ttl > 0);
return store.set(key, val, ttl);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since store isn't actually needed, perhaps the following would be simpler?

const cache = {
  get: () => {},
  delete: () => {},
  set: (key, val, ttl) => {
    // your set() code
  }
};

},
delete: store.delete.bind(store)
};

t.plan(2);

await got(s.url + endpoint, {cache});
});

test('Cached response is re-encoded to current encoding option', async t => {
const endpoint = '/cache';
const cache = new Map();
const firstEncoding = 'base64';
const secondEncoding = 'hex';

const firstResponse = await got(s.url + endpoint, {cache, encoding: firstEncoding});
const secondResponse = await got(s.url + endpoint, {cache, encoding: secondEncoding});

const expectedSecondResponseBody = Buffer.from(firstResponse.body, firstEncoding).toString(secondEncoding);

t.is(cache.size, 1);
t.is(secondResponse.body, expectedSecondResponseBody);
});

test('Stale cache entries with Last-Modified headers are revalidated', async t => {
const endpoint = '/last-modified';
const cache = new Map();

const firstResponse = await got(s.url + endpoint, {cache});
const secondResponse = await got(s.url + endpoint, {cache});

t.is(cache.size, 1);
t.is(firstResponse.statusCode, 200);
t.is(secondResponse.statusCode, 304);
t.is(firstResponse.body, 'last-modified');
t.is(firstResponse.body, secondResponse.body);
});

test('Stale cache entries with ETag headers are revalidated', async t => {
const endpoint = '/etag';
const cache = new Map();

const firstResponse = await got(s.url + endpoint, {cache});
const secondResponse = await got(s.url + endpoint, {cache});

t.is(cache.size, 1);
t.is(firstResponse.statusCode, 200);
t.is(secondResponse.statusCode, 304);
t.is(firstResponse.body, 'etag');
t.is(firstResponse.body, secondResponse.body);
});

test('Stale cache entries that can\'t be revalidate are deleted from cache', async t => {
const endpoint = '/cache-then-no-store-on-revalidate';
const cache = new Map();

const firstResponse = await got(s.url + endpoint, {cache});
t.is(cache.size, 1);
const secondResponse = await got(s.url + endpoint, {cache, log: true});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a newline above too?


t.is(cache.size, 0);
t.is(firstResponse.statusCode, 200);
t.is(secondResponse.statusCode, 200);
t.is(firstResponse.body, 'cache-then-no-store-on-revalidate');
t.is(firstResponse.body, secondResponse.body);
});

test('Response objects have fromCache property set correctly', async t => {
const endpoint = '/cache';
const cache = new Map();

const response = await got(s.url + endpoint, {cache});
const cachedResponse = await got(s.url + endpoint, {cache});

t.false(response.fromCache);
t.true(cachedResponse.fromCache);
});

test.after('cleanup', async () => {
await s.close();
});