Coder Social home page Coder Social logo

kwhitley / apicache Goto Github PK

View Code? Open in Web Editor NEW
1.2K 17.0 193.0 1.05 MB

Simple API-caching middleware for Express/Node.

License: MIT License

JavaScript 100.00%
javascript api cache json rest response middleware restify redis memory fast node express

apicache's Introduction

A simple API response caching middleware for Express/Node using plain-english durations.

Supports Redis or built-in memory engine with auto-clearing.

npm version node version support Build Status via Travis CI Coverage Status NPM downloads

Why?

Because route-caching of simple data/responses should ALSO be simple.

Usage

To use, simply inject the middleware (example: apicache.middleware('5 minutes', [optionalMiddlewareToggle])) into your routes. Everything else is automagic.

Cache a route

import express from 'express'
import apicache from 'apicache'

let app = express()
let cache = apicache.middleware

app.get('/api/collection/:id?', cache('5 minutes'), (req, res) => {
  // do some work... this will only occur once per 5 minutes
  res.json({ foo: 'bar' })
})

Cache all routes

let cache = apicache.middleware

app.use(cache('5 minutes'))

app.get('/will-be-cached', (req, res) => {
  res.json({ success: true })
})

Use with Redis

import express from 'express'
import apicache from 'apicache'
import redis from 'redis'

let app = express()

// if redisClient option is defined, apicache will use redis client
// instead of built-in memory store
let cacheWithRedis = apicache.options({ redisClient: redis.createClient() }).middleware

app.get('/will-be-cached', cacheWithRedis('5 minutes'), (req, res) => {
  res.json({ success: true })
})

Cache grouping and manual controls

import apicache from 'apicache'
let cache = apicache.middleware

app.use(cache('5 minutes'))

// routes are automatically added to index, but may be further added
// to groups for quick deleting of collections
app.get('/api/:collection/:item?', (req, res) => {
  req.apicacheGroup = req.params.collection
  res.json({ success: true })
})

// add route to display cache performance (courtesy of @killdash9)
app.get('/api/cache/performance', (req, res) => {
  res.json(apicache.getPerformance())
})

// add route to display cache index
app.get('/api/cache/index', (req, res) => {
  res.json(apicache.getIndex())
})

// add route to manually clear target/group
app.get('/api/cache/clear/:target?', (req, res) => {
  res.json(apicache.clear(req.params.target))
})

/*

GET /api/foo/bar --> caches entry at /api/foo/bar and adds a group called 'foo' to index
GET /api/cache/index --> displays index
GET /api/cache/clear/foo --> clears all cached entries for 'foo' group/collection

*/

Use with middleware toggle for fine control

// higher-order function returns false for responses of other status codes (e.g. 403, 404, 500, etc)
const onlyStatus200 = (req, res) => res.statusCode === 200

const cacheSuccesses = cache('5 minutes', onlyStatus200)

app.get('/api/missing', cacheSuccesses, (req, res) => {
  res.status(404).json({ results: 'will not be cached' })
})

app.get('/api/found', cacheSuccesses, (req, res) => {
  res.json({ results: 'will be cached' })
})

Prevent cache-control header "max-age" from automatically being set to expiration age

let cache = apicache.options({
  headers: {
    'cache-control': 'no-cache',
  },
}).middleware

let cache5min = cache('5 minute') // continue to use normally

API

  • apicache.options([globalOptions]) - getter/setter for global options. If used as a setter, this function is chainable, allowing you to do things such as... say... return the middleware.
  • apicache.middleware([duration], [toggleMiddleware], [localOptions]) - the actual middleware that will be used in your routes. duration is in the following format "[length][unit]", as in "10 minutes" or "1 day". A second param is a middleware toggle function, accepting request and response params, and must return truthy to enable cache for the request. Third param is the options that will override global ones and affect this middleware only.
  • middleware.options([localOptions]) - getter/setter for middleware-specific options that will override global ones.
  • apicache.getPerformance() - returns current cache performance (cache hit rate)
  • apicache.getIndex() - returns current cache index [of keys]
  • apicache.clear([target]) - clears cache target (key or group), or entire cache if no value passed, returns new index.
  • apicache.newInstance([options]) - used to create a new ApiCache instance (by default, simply requiring this library shares a common instance)
  • apicache.clone() - used to create a new ApiCache instance with the same options as the current one

Available Options (first value is default)

{
  debug:            false|true,     // if true, enables console output
  defaultDuration:  '1 hour',       // should be either a number (in ms) or a string, defaults to 1 hour
  enabled:          true|false,     // if false, turns off caching globally (useful on dev)
  redisClient:      client,         // if provided, uses the [node-redis](https://github.com/NodeRedis/node_redis) client instead of [memory-cache](https://github.com/ptarjan/node-cache)
  appendKey:        fn(req, res),   // appendKey takes the req/res objects and returns a custom value to extend the cache key
  headerBlacklist:  [],             // list of headers that should never be cached
  statusCodes: {
    exclude:        [],             // list status codes to specifically exclude (e.g. [404, 403] cache all responses unless they had a 404 or 403 status)
    include:        [],             // list status codes to require (e.g. [200] caches ONLY responses with a success/200 code)
  },
  trackPerformance: false,          // enable/disable performance tracking... WARNING: super cool feature, but may cause memory overhead issues
  headers: {
    // 'cache-control':  'no-cache' // example of header overwrite
  },
  respectCacheControl: false|true   // If true, 'Cache-Control: no-cache' in the request header will bypass the cache.
}
*Optional: Typescript Types (courtesy of @danielsogl)
$ npm install -D @types/apicache

Custom Cache Keys

Sometimes you need custom keys (e.g. save routes per-session, or per method). We've made it easy!

Note: All req/res attributes used in the generation of the key must have been set previously (upstream). The entire route logic block is skipped on future cache hits so it can't rely on those params.

apicache.options({
  appendKey: (req, res) => req.method + res.session.id,
})

Cache Key Groups

Oftentimes it benefits us to group cache entries, for example, by collection (in an API). This would enable us to clear all cached "post" requests if we updated something in the "post" collection for instance. Adding a simple req.apicacheGroup = [somevalue]; to your route enables this. See example below:

var apicache = require('apicache')
var cache = apicache.middleware

// GET collection/id
app.get('/api/:collection/:id?', cache('1 hour'), function(req, res, next) {
  req.apicacheGroup = req.params.collection
  // do some work
  res.send({ foo: 'bar' })
})

// POST collection/id
app.post('/api/:collection/:id?', function(req, res, next) {
  // update model
  apicache.clear(req.params.collection)
  res.send('added a new item, so the cache has been cleared')
})

Additionally, you could add manual cache control to the previous project with routes such as these:

// GET apicache index (for the curious)
app.get('/api/cache/index', function(req, res, next) {
  res.send(apicache.getIndex())
})

// GET apicache index (for the curious)
app.get('/api/cache/clear/:key?', function(req, res, next) {
  res.send(200, apicache.clear(req.params.key || req.query.key))
})

Debugging/Console Out

Using Node environment variables (plays nicely with the hugely popular debug module)

$ export DEBUG=apicache
$ export DEBUG=apicache,othermoduleThatDebugModuleWillPickUp,etc

By setting internal option

import apicache from 'apicache'

apicache.options({ debug: true })

Client-Side Bypass

When sharing GET routes between admin and public sites, you'll likely want the routes to be cached from your public client, but NOT cached when from the admin client. This is achieved by sending a "x-apicache-bypass": true header along with the requst from the admin. The presence of this header flag will bypass the cache, ensuring you aren't looking at stale data.

Contributors

Special thanks to all those that use this library and report issues, but especially to the following active users that have helped add to the core functionality!

  • @Chocobozzz - the savior of getting this to pass all the Node 14/15 tests again... thanks for everyone's patience!!!
  • @killdash9 - restify support, performance/stats system, and too much else at this point to list
  • @svozza - added restify tests, test suite refactor, and fixed header issue with restify. Node v7 + Restify v5 conflict resolution, etag/if-none-match support, etcetc, etc. Triple thanks!!!
  • @andredigenova - Added header blacklist as options, correction to caching checks
  • @peteboere - Node v7 headers update
  • @rutgernation - JSONP support
  • @enricsangra - added x-apicache-force-fetch header
  • @tskillian - custom appendKey path support
  • @agolden - Content-Encoding preservation (for gzip, etc)
  • @davidyang - express 4+ compatibility
  • @nmors - redis support
  • @maytis, @ashwinnaidu - redis expiration
  • @ubergesundheit - Corrected buffer accumulation using res.write with Buffers
  • @danielsogl - Keeping dev deps up to date, Typescript Types
  • @vectart - Added middleware local options support
  • @davebaol - Added string support to defaultDuration option (previously just numeric ms)
  • @Rauttis - Added ioredis support
  • @fernandolguevara - Added opt-out for performance tracking, great emergency fix, thank you!!

Bugfixes, tweaks, documentation, etc.

  • @Amhri, @Webcascade, @conmarap, @cjfurelid, @scambier, @lukechilds, @Red-Lv, @gesposito, @viebel, @RowanMeara, @GoingFast, @luin, @keithws, @daveross, @apascal, @guybrush

Changelog

  • v1.6.0 - added respectCacheControl option flag to force honoring no-cache (thanks @NaridaL!)
  • v1.5.4 - up to Node v15 support, HUGE thanks to @Chocobozzz and all the folks on the PR thread! <3
  • v1.5.3 - multiple fixes: Redis should be connected before using (thanks @guybrush)
  • v1.5.2 - multiple fixes: Buffer deprecation and _headers deprecation, { trackPerformance: false } by default per discussion (sorry semver...)
  • v1.5.1 - adds { trackPerformance } option to enable/disable performance tracking (thanks @fernandolguevara)
  • v1.5.0 - exposes apicache.getPerformance() for per-route cache metrics (@killdash9 continues to deliver)
  • v1.4.0 - cache-control header now auto-decrements in cached responses (thanks again, @killdash9)
  • v1.3.0 - [securityfix] apicache headers no longer embedded in cached responses when NODE_ENV === 'production' (thanks for feedback @satya-jugran, @smddzcy, @adamelliotfields). Updated deps, now requiring Node v6.00+.
  • v1.2.6 - middlewareToggle() now prevents response block on cache hit + falsy toggle (thanks @apascal)
  • v1.2.5 - uses native Node setHeader() rather than express.js header() (thanks @keithws and @daveross)
  • v1.2.4 - force content type to Buffer, using old and new Buffer creation syntax
  • v1.2.3 - add etag to if-none-match 304 support (thanks for the test/issue @svozza)
  • v1.2.2 - bugfix: ioredis.expire params (thanks @GoingFast and @luin)
  • v1.2.1 - Updated deps
  • v1.2.0 - Supports ioredis (thanks @Rauttis)
  • v1.1.1 - bugfixes in expiration timeout clearing and content header preservation under compression (thanks @RowanMeara and @samimakicc).
  • v1.1.0 - added the much-requested feature of a custom appendKey function (previously only took a path to a single request attribute). Now takes (request, response) objects and returns some value to be appended to the cache key.
  • v1.0.0 - stamping v0.11.2 into official production version, will now begin developing on branch v2.x (redesign)
  • v0.11.2 - dev-deps update, courtesy of @danielsogl
  • v0.11.1 - correction to status code caching, and max-age headers are no longer sent when not cached. middlewareToggle now works as intended with example of statusCode checking (checks during shouldCacheResponse cycle)
  • v0.11.0 - Added string support to defaultDuration option, previously just numeric ms - thanks @davebaol
  • v0.10.0 - added ability to blacklist headers (prevents caching) via options.headersBlacklist (thanks @andredigenova)
  • v0.9.1 - added eslint in prep for v1.x branch, minor ES6 to ES5 in master branch tests
  • v0.9.0 - corrected Node v7.7 & v8 conflicts with restify (huge thanks to @svozza for chasing this down and fixing upstream in restify itself). Added coveralls. Added middleware.localOptions support (thanks @vectart). Added ability to overwrite/embed headers (e.g. "cache-control": "no-cache") through options.
  • v0.8.8 - corrected to use node v7+ headers (thanks @peteboere)
  • v0.8.6, v0.8.7 - README update
  • v0.8.5 - dev dependencies update (thanks @danielsogl)
  • v0.8.4 - corrected buffer accumulation, with test support (thanks @ubergesundheit)
  • v0.8.3 - added tests for x-apicache-bypass and x-apicache-force-fetch (legacy) and fixed a bug in the latter (thanks @Red-Lv)
  • v0.8.2 - test suite and mock API refactor (thanks @svozza)
  • v0.8.1 - fixed restify support and added appropriate tests (thanks @svozza)
  • v0.8.0 - modifies response accumulation (thanks @killdash9) to support res.write + res.end accumulation, allowing integration with restify. Adds gzip support (Node v4.3.2+ now required) and tests.
  • v0.7.0 - internally sets cache-control/max-age headers of response object
  • v0.6.0 - removed final dependency (debug) and updated README
  • v0.5.0 - updated internals to use res.end instead of res.send/res.json/res.jsonp, allowing for any response type, adds redis tests
  • v0.4.0 - dropped lodash and memory-cache external dependencies, and bumped node version requirements to 4.0.0+ to allow Object.assign native support

apicache's People

Contributors

agolden avatar amri91 avatar apascal avatar arman92 avatar ashwinnaidu avatar briangonzalez avatar chocobozzz avatar danielsogl avatar davebaol avatar davidyang avatar enricsangra avatar fernandolguevara avatar gesposito avatar guybrush avatar killdash9 avatar kwhitley avatar lukechilds avatar paulcpk avatar peteboere avatar rauttis avatar rowanmeara avatar rutgernation avatar samimakicc avatar svozza avatar tskillian avatar ubergesundheit avatar vectart avatar viebel avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

apicache's Issues

Fails to cache 'content-encoding' header

Apicache is failing to cache the 'content-encoding' header. As a result, when you retrieve something from cache that is gzipped, the browser (or whoever is viewing the document from cache) does not realize that the content should be decompressed before display.

bypass for errors

I'm using apicache to cache an expense request to an upstream server that sometimes times out. However when the upstream request does timeout, I want to return an error response and not cache it so that the next time I will retry the upstream operation again.

So it's not clear how to say from my route handler post-apicache middleware, "don't cache the request this time." I've tried adding apicache.clear(req.url); in my handler when I return my error response but that doesn't appear to work.

Update usage in README?

Should this section be updated? In my experience, this caches all routes defined after.

import apicache from 'apicache'
let cache = apicache.middleware

- app.use(cache('5 minutes'))

// routes are automatically added to index, but may be further added
// to groups for quick deleting of collections
app.get('/api/:collection/:item?', (req, res) => {
  req.apicacheGroup = req.params.collection
  res.json({ success: true })
})

// add route to display cache index
app.get('/api/cache/index', (req, res) => {
  res.json(apicache.getIndex())
})

// add route to manually clear target/group
app.get('/api/cache/clear/:target?', (req, res) => {
  res.json(apicache.clear(req.params.target))
})

+ app.use(cache('5 minutes'))

FEATURE: Add cache-control routing middleware

Rather than manually wiring up individual routes for exposing cache, I'd like to include an apicache.control middleware for easy injection into a project.

Example:

import express from 'express';
import apicache from 'apicache';

const app = express();

// basic example
app.use('/api/cache', apicache.control);

// basic example with authentication middleware to prevent unauthorized cache purges, etc
app.use('/api/cache', authMiddleware, apicache.control);

/* PROPOSED ROUTES
../cache/index --> shows index
../cache/clear --> clears all cache
../cache/clear/{group} --> clears cache group (if grouping used)
../cache/expires/{time} --> ??? set default timeout on the fly
*/

Index is not rebuilding after app restart

When app restart cache index is not rebuilt. That is a problem when using redis cache, because without index I cannot clear the cache and I have to wait for items to expire in redis. Maybe index should be stored in redis too? Also if you are scaling to have many application instances, the index should be shared between them.

Trying to cache for more than 30 days causes all misses

The source of issue appears to be that setTimeout breaks on any millisecond value that doesn't fit in a 32-bit int. See: ptarjan/node-cache#35

If there's no easy workaround (I suspect not), it'd be good to mention this limitation in the docs since it looks like the apicache code is designed to handle longer time periods like months/years.

As an aside it would be nice to have a "cache indefinitely until cleared" option โ€“ my original use case for setting such a long time in the first place. memory-cache supports it by omitting the time argument.

Should If-None-Match Headers Be Respected?

As it stands, the apicache middleware breaks Express's automatic handling of etags when the client supplies an If-None-Match header. I'm presuming it's something to do with overiding res.end but either way this is a useful feature that can reduce data sent to the client, which is especially useful in bandwidth constrained scenarios like mobile apps. It's a shame to have to chose between reducing traffic to the backend or payloads to the client. Here's a failing test case:

it('respects if-none-match header', function() {
        var app = mockAPI.create('10 seconds')

        return request(app)
          .get('/api/movies')
          .expect(200)
          .then(function(res) {
            return res.headers['etag']
          })
          .then(function(etag) {
            return request(app)
              .get('/api/movies')
              .set('if-none-match', etag)
              .expect(304)
              .expect('etag', etag)
          })
      })

Path not being cached

I've tried following your instructions and I think I've done it right yet nothing is getting cached. Am I doing something wrong or is this a bug? Below is the code in which I am trying to use your cache. You'll see one line with req.apicacheGroup commented out. I'd like to use this but disabled it in trying to debug stuff. The response given is a page full of JSON (not just a single key: value). Thanks for any insight you can provide.

var apicache  = require('apicache').options({ debug: true }).middleware;
var converter = require('csvtojson').core.Converter;
var express   = require('express');
var request   = require('request');
var router    = express.Router();


/* 
 *  GET home page.
 */
router.get('/', function(req, res) {
  res.render('index');
});


/*
 *  Routes per school
 */

// Pull in the file(s) from each school
var westga = require('./schools/westga');

// westga's routes
router.get('/westga/bulletin/:term', apicache('60 minutes'), function(req, res) {
  //req.apicacheGroup = req.params.term
  var term = req.params.term;
  if ( westga.validTerms.indexOf(term) >= 0 ) {
    res.writeHead(200, {"Content-Type": "application/json"});
    switch (term) {
      case 'fall':
        var url = westga.bulletin + westga.fall;
        break;
      case 'spring':
        var url = westga.bulletin + westga.spring;
        break;
      case 'summer':
        var url = westga.bulletin + westga.summer;
        break;
      default:
        console.log('Something went wrong...');
    }
    // Don't save everything to memory. This facilitates large CSV's
    var csvConverterForWestgaBulletin = new converter({constructResult:false});
    console.log(url);
    request.get(url).pipe(csvConverterForWestgaBulletin).pipe(res);
  } else {
    res.status(404);
    res.render('404.jade', {title: '404: File Not Found'});
    console.log('invalid term provided');
  }
});

router.get('/westga/catalog', apicache('1 month'), function(req, res) {
  var url = westga.catalog + westga.currentCatalog;
  // Don't save everything to memory. This facilitates large CSV's
  var csvConverterForWestgaCatalog = new converter({constructResult:false});
  console.log(url);
  res.writeHead(200, {"Content-Type": "application/json"});
  request.get(url).pipe(csvConverterForWestgaCatalog).pipe(res);
});

module.exports = router;

response.write handling of Buffer type not correct

Hi,

I have a Stream which is piped into the response which emits Buffers with a single String at the end.
I have added an else if block to handle chunks of type Buffer. Could you please add it to the package and release a new version?

Cheers

  function accumulateContent(res, content) {
    if (content) {
      if (typeof(content) == 'string') {
        res._apicache.content = (res._apicache.content || '') + content;
      } else if (Buffer.isBuffer(content)) {
        var oldContent = res._apicache.content
        if (!oldContent) {
          oldContent = Buffer.alloc(0);
        }
        res._apicache.content = Buffer.concat([oldContent, content], oldContent.length + content.length);
      } else {
        res._apicache.content = content
        // res._apicache.cacheable = false;
      }
    }
  }

Caching POST and breaking on Body change

We have an Express Node Api which incudes diffrent controllers for API using Routes.Index.js

So when we Implement ApiCache it seems to return same Response even if we change request Parameter..

E.g :
Login API : ApiCache implemented
When we request first time we get data from API with Credentials like Username : aFirst and Password : aFirst
When we request SECOND time we get data from API with Credentials like Username : aSecond and Password : aSecond

It should Refresh cache when we change Input Parameter for GET and POST API.

Thanks.

Duplicated entries at index

Hi,

why when a cached URL is expired, it is still in the index?.

Example:
router.get('/', cache(2 seconds), function(req, res)

Now, I do 4 request, and getIndex returns:

"all": [
      "/agents",
      "/agents"
    ],
    "groups": {
      "agents": [
        "/agents",
        "/agents"
      ]
    }

why there are 2 requests ("/agents") instead of just 1 request?. all the request showed in getIndex are in memory?. It seems like old cached requests are not deleted from the memory.

Thanks.

Cannot read property 'x-apicache-bypass' of undefined

Hi,

Having a bit of an issue when trying to implement. My sample code is as follows:

var express = require('express');
var apicache = require('apicache');
apicache.options({ debug: true });
var cache = apicache.middleware();
var router = express.Router();

router.get('/test', cache('1 hour'), function (req, res) {
//code in here
};

When the app loads, I get the error 'Cannot read property 'x-apicache-bypass' of undefined'.

Any idea on what I might be missing?

uncaughtException The header content contains invalid characters

We are trying out Api cache and its working perfectly with redis client. But once we plugged in the SSO node module, read operation fails abruptly, i can see [api-cache]: returning redis cached version of "/api/userdetails" but something going wrong after that.

If i remove the SSO implementation everything works fine.

This is the first time i'm trying out api cache with the app which works fine with SSO module.

Please share your thoughts regarding what might be going wrong

[api-cache]: returning redis cached version of "/api/userdetails"
Thu, 11 Aug 2016 16:26:17 GMT uncaughtException The header content contains invalid characters
TypeError: The header content contains invalid characters
    at ServerResponse.OutgoingMessage.setHeader (_http_outgoing.js:348:11)
    at ServerResponse.header (C:\Projects\hub\wayne\hub-webapp\node_modules\express\lib\response.js:719:10)
    at ServerResponse.header (C:\Projects\hub\wayne\hub-webapp\node_modules\express\lib\response.js:722:12)
    at C:\Projects\hub\wayne\hub-webapp\node_modules\apicache\lib\apicache.js:170:21
    at tryCatcher (C:\Projects\hub\wayne\hub-webapp\node_modules\bluebird\js\release\util.js:16:23)
    at Promise.successAdapter [as _fulfillmentHandler0] (C:\Projects\hub\wayne\hub-webapp\node_modules\bluebird\js\release\nodeify.js:23:30)
    at Promise._settlePromise (C:\Projects\hub\wayne\hub-webapp\node_modules\bluebird\js\release\promise.js:558:21)
    at Promise._settlePromise0 (C:\Projects\hub\wayne\hub-webapp\node_modules\bluebird\js\release\promise.js:606:10)
    at Promise._settlePromises (C:\Projects\hub\wayne\hub-webapp\node_modules\bluebird\js\release\promise.js:685:18)
    at Async._drainQueue (C:\Projects\hub\wayne\hub-webapp\node_modules\bluebird\js\release\async.js:138:16)
    at Async._drainQueues (C:\Projects\hub\wayne\hub-webapp\node_modules\bluebird\js\release\async.js:148:10)
    at Immediate.Async.drainQueues [as _onImmediate] (C:\Projects\hub\wayne\hub-webapp\node_modules\bluebird\js\release\async.js:17:14)
    at tryOnImmediate (timers.js:543:15)
    at processImmediate [as _immediateCallback] (timers.js:523:5)

Support caching other data types besides JSON

I was thinking, it may be a good idea to add support for binary or other types, rather then only JSON.

Redis claims to have good support for binary types, (see http://redis.io/topics/data-types) so that would mean that endocding/decoding would be unnecessary

We would also need a mechanism to store the mime-type (edit:) / Content-Type header

How to cache callback result

Hi all. Need advise.
I'm trying to cache like this, but nothing happens.

const express = require('express');
const router = express.Router();
const apicache = require('apicache');
const backend = require('../integrations/backend');
let cache = apicache.middleware;

router.get('/events', cache('5 minutes'), function(req, res, next) {
    backend.getEvents('',(result, error) => {
        if (error) {
            return;
        }
        res.setHeader('Content-Type', 'application/json');
        res.send(result);
    });
});

But if a get info from backend in sync manner - cache works.

Calling clear() without specified group fails

globalOptions.redisClient.del(); fails with:

ReplyError: ERR wrong number of arguments for 'del' command
    at parseError (/home/fiouch/diplomski-fvoska/node_modules/redis-parser/lib/parser.js:181:12)
    at parseType (/home/fiouch/diplomski-fvoska/node_modules/redis-parser/lib/parser.js:269:14)

Redis DEL command requires parameters, as per documentation.

Not sure if this has changed over time as I have only recently started using redis.

  • apicache is at 0.3.6
  • redis is at 2.6.3
  • redis-parser is at 2.3.0
  • redis-server 3.0.7 is running on Ubuntu 16.04

I temporarily fixed it on my end by replacing globalOptions.redisClient.del(); with globalOptions.redisClient.flushall(); But this flushes whole redis, not only apicache items. DEL should get all keys used in apicache (maybe get from index.all?) as parameters.

Thoughts: apicache returning class constructor instead of fixed instance

This would allow each segment of your app to have a unique instance (complete with rules, index, etc) rather than sharing one...

Also hugely helpful with creating unit tests.

Thoughts?

Obviously unless I can figure out a clever way to do this, this would count as a major breaking change and be slated for the future major release version.

Problem with Redis and cache key groups

With the memory store, this code works:

const apicache = require('apicache');
const cache = apicache.middleware;

router.get('/someroute', cache('1 hour'), (req, res) => {
  req.apicacheGroup = 'cachekey'; // Set cache
  // ...
});

router.post('/someroute/', (req, res) => {
  apicache.clear('cachekey'); // Clear cache
  // ...
});

But if I set the redisClient option, the apicache.clear('cachekey') function does not clear the cache as expected:

const redis = require('redis').createClient();
apicache.options({redisClient: redis}); // This breaks

Middleware toggle

Hi! I have some question, mb i didn't understand something.

const onlyStatus200 = (req, res) => res.statusCode === 200

const cacheSuccesses = cache('5 minutes', onlyStatus200)

app.get('/api/missing', cacheSuccesses, (req, res) => {
  res.status(404).json({ results: 'will not be cached' })
})
  this.middleware = function cache(strDuration, middlewareToggle) {
    var duration = instance.getDuration(strDuration)

    return function cache(req, res, next) {
      function bypass() {
        debug('bypass detected, skipping cache.')
        return next()
      }

      // initial bypass chances
      if (!globalOptions.enabled) return bypass()
      if (req.headers['x-apicache-bypass'] || req.headers['x-apicache-force-fetch']) return bypass()
      if (typeof middlewareToggle === 'function') {
        if (!middlewareToggle(req, res)) return bypass()
      } else if (middlewareToggle !== undefined && !middlewareToggle) {
        return bypass()
      }

So onlyStatus200 middleware is called before we have some operations with response. And it would be always 200 as initial, so actually all statuses would be cached.
Option statusCodes: [ include: [200] ] works as should.
Thanks.

REQUEST: Tests

Huge request to you guys out there, now that this actually has enough of a user-base to help support it:

I would LOVE someone to write a test suite for this (using whatever flavor they prefer, although I've always preferred mocha myself).

...

For those that don't know, I wrote this library years ago to support my own blog engine that sported an (obviously) very simple JSON API. Since then I've been torn a million ways, both in my role as a developer (which took me overseas to Kuwait for the last two years), as well as my passion (and future career) in wilderness fine art photography. I appreciate each and every issue, PR, etc that everyone has brought up, and offer my sincerest apologies for not being able to respond in anything even close to a timely manner. I'm trying to update everything right now, merge PRs, update docs, and ultimately tag a new release. If anyone would like to take on a more active role as contributor to this library, updating it and expanding features (while preserving the simplicity around which it was first designed), let's chat!

Cheers,
Kevin
http://krwhitley.com
https://www.facebook.com/kevin.r.whitley

Function gets called on every request if all requests are 304

Using code:

app.get('/api/collection/:id?', cache('5 minutes'), (req, res) => {
  console.log("hit")
  res.json({ foo: 'bar' })
})
  1. start server
  2. load page in browser
  3. hit refresh
  4. observe hit is only outputted at most every 5 minutes as expected
  5. stop server but keep browser open
  6. start server
  7. hit refresh on browser
  8. observe on every refresh, hit is outputted which is not expected

Apicache overwrites the res.end and res.write methods in makeResponseCacheable in apicache.js but these methods are called with empty string as content when response is 304 but hit is still displayed. Is there anyway to workaround this?

The cache should give out "leases" if same URL is requested concurrently

Right now, if you request the same URL concurrently, both requests will bypass the cache and drop into the express route. I think it would be preferable if only the request seen first by express drops into the route, and the 2nd request will await the response from the route, and return that.

I'm thinking that it should be possible to create a shared promise per URL, that once resolved, returns JSON to the clients. This way, 1000 concurrent requests would look like 1 request to the underlying app. I'm thinking that this is the more preferable than current behaviour.

This becomes a significant problem if you have a relatively short cache duration with a lot of concurrent requests, since every cache-duration seconds, your app will halt to a crawl before new cache keys get inserted.

Null error in sendCachedResponse

Im not exactly sure what is causing the issue but on occasion I get the following error.

TypeError: Cannot convert undefined or null to object
at Function.assign ()
at sendCachedResponse (C:\Users\Matthew\Desktop\CDMMS\node_modules\apicache\src\apicache.js:129:12)
at cache (C:\Users\Matthew\Desktop\CDMMS\node_modules\apicache\src\apicache.js:281:18)
at Layer.handle [as handle_request] (C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\layer.js:95:5)
at next (C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\route.js:131:13)
at Route.dispatch (C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\route.js:112:3)
at Layer.handle [as handle_request] (C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\layer.js:95:5)
at C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\index.js:277:22
at Function.process_params (C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\index.js:330:12)
at next (C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\index.js:271:10)
at Function.handle (C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\index.js:176:3)
at router (C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\index.js:46:12)
at Layer.handle [as handle_request] (C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\layer.js:95:5)
at trim_prefix (C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\index.js:312:13)
at C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\index.js:280:7
at Function.process_params (C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\index.js:330:12)
at next (C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\index.js:271:10)
at Function.handle (C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\index.js:176:3)
at router (C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\index.js:46:12)
at Layer.handle [as handle_request] (C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\layer.js:95:5)
at trim_prefix (C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\index.js:312:13)
at C:\Users\Matthew\Desktop\CDMMS\node_modules\express\lib\router\index.js:280:7

Editing sendCachedResponse to include a check for null seems to resolve the issue

function sendCachedResponse(response, cacheObject) {
if (response._headers != null) {
Object.assign(response._headers, cacheObject.headers, {
'apicache-store': globalOptions.redisClient ? 'redis' : 'memory',
'apicache-version': pkg.version
})
}

Cache groups & index only work in debug mode

In my testing I am finding that the index object and the cache groups (apicacheGroup) are both only populated in debug mode.

Is this intentional? It seems to me that the console.log should be behind the 'if debug mode', but not the other code in that section. Perhaps the two 'if' statements are the wrong way around?

Either that or I am doing something wrong...

Thanks in advance for any feedback.

Redis support

Hi all.

I heard that there was a future plan to implement redis as the cache store. Is anyone working on this at the moment?

I am running a clustered express api and the cache needs to be common across all processes.

I will make this change and provide a PR if nobody else is going to soon.

Ill provide an optional parameter which is a redis client instance. So redis client will be maintained outside of the module. Is this okay? Else i could include the redis client inside apicache and simply pass options instead.

Let me know which way is preferred

Thanks
Nathan

Typo in code (version 0.5.1)

Sorry, I don't have time at all to make a PR.

src/apicache.js, line 166
var group = index.groupss[groupName]
should be
var group = index.groups[groupName]

That's a nice project btw, thanks for making it.

Headers Being Dropped In Restify

With Restify 4.3 I've encountered an issue with headers being dropped when a response is being returned from the cache. I've localised the issue to here:

function sendCachedResponse(response, cacheObject) {
    Object.assign(response._headers || {}, cacheObject.headers || {}, {
      'apicache-store': globalOptions.redisClient ? 'redis' : 'memory',
      'apicache-version': pkg.version
    })

    // unstringify buffers
    var data = cacheObject.data
    if (data && data.type === 'Buffer') {
      data = new Buffer(data.data)
    }

    response.writeHead(cacheObject.status || 200, response._headers)

    return response.end(data, cacheObject.encoding)
  }

When response._headers is null the code falls back to {} but because the result of Object.assign is not saved in a variable it gets dropped silently. The reason why we don't see this in Express is because it always adds the X-Powered-By: 'Express' header so res._headers is never empty (although anyone who disables this feature will see this problem).

The solution is very simple and I'd be more than happy to raise a PR for this:

function sendCachedResponse(response, cacheObject) {
    // Should the res object be mutated here rather than cloned as I've done?
    const headers = Object.assign({}. response._headers || {}, cacheObject.headers || {}, {
      'apicache-store': globalOptions.redisClient ? 'redis' : 'memory',
      'apicache-version': pkg.version
    })
    // ...
    response.writeHead(cacheObject.status || 200, headers)
    // ...
  }

add apicache headers to response

In particular, for testing sake and the ability to tell at a glance (from the client) if you're seeing a cached version or not - I'm going to embed (and namespace) some apicache response headers:

Thinking of something along the lines of:

{
  apicache: 'v0.1.0',         // on cached responses only
  apicache-duration: '1 week' // because who reads in milliseconds?
}

Suggestion: ignore hosts

I am happy with apicache module . especially the part where you can write human readable cache expire.
I am also having trouble with some couchdb/pouchdb backends which return 201 when posted , I can

statusCodes: {
       exclude: ['201']
}

somehow same url returns 200 and future posts are ignored
my suggestion is to add ignore_hosts functionality
Thanks
Edit: Ignore methods as POST also would be great.

Cache-Control problems

Hey @kwhitley I was trying to use this library, but what I see it sets "Cache-Control" to chosen cache duration. I was intending to spare the server heavy database tasks and the cache is controlled by the server, meaning it could be cleared anytime. Setting "Cache-Control" with fixed max-age prevents some clients from hitting my endpoints, even when the cache was cleared by the server. Any workarounds? I believe Cache-Control shouldn't be set in this library or it should be an option.

Serve Different sub-domains

Hi,

So, I am trying to serve different sub-domain from the same express server. The subdomains are language and country based (en-sa, ar-sa ...) and am parsing the context into a jade file before sending to client.

The issue is, as obvious, the caching is route based so for homepage it serves the same html for all sub-domains, '/'.

How do you think I can handle this to cache different homepages for different sub-domains ?

Information Request: How do I add an extra Item to the Route (Like Company ID)

Hi, I have the same routes for different companies and users. We get the Company ID (and the user id in other cases) from a security token given on login.

And then we filter by this Company ID taken from a token that comes in a HEADER on the REQUEST.

How could I add that (a mondodb ID)? so something like this 52911feb8f819b571000000b

Is there something like:

import apicache from 'apicache'
let cache = apicache.middleware

app.use(cache('5 minutes'))

// routes are automatically added to index, but may be further added
// to groups for quick deleting of collections
app.get('/api/articles', (req, res) => {
  req.apicacheKey = '{5720f4980882091000b66c40}/api/articles'
  req.apicacheGroup = req.params.collection
  res.json({ success: true })
})

Strange behavior (v0.0.14) duplicate add cache message, strange order of messages, cache does not timeout

Hi,

These are the messages I am getting in the console. The calls are roughly 1 second apart.

[api-cache]: returning redis cached version of "/firstApi?param=value/appendKey=value"
[api-cache]: path "/firstApi?param=value/appendKey=value" not found in cache
[api-cache]: adding cache entry for "/firstApi?param=value/appendKey=value" @ 5000 milliseconds
[api-cache]: adding cache entry for "/firstApi?param=value/appendKey=value" @ 5000 milliseconds
[api-cache]: returning redis cached version of "/firstApi?param=value/appendKey=value"
[api-cache]: returning redis cached version of "/firstApi?param=value/appendKey=value"
[api-cache]: returning redis cached version of "/firstApi?param=value/appendKey=value"
[api-cache]: returning redis cached version of "/firstApi?param=value/appendKey=value"
[api-cache]: returning redis cached version of "/firstApi?param=value/appendKey=value"
[api-cache]: returning redis cached version of "/firstApi?param=anotherValue/appendKey=anotherValue"
[api-cache]: path "/firstApi?param=anotherValue/appendKey=anotherValue" not found in cache
[api-cache]: adding cache entry for "/firstApi?param=anotherValue/appendKey=anotherValue" @ 5000 milliseconds
[api-cache]: adding cache entry for "/firstApi?param=anotherValue/appendKey=anotherValue" @ 5000 milliseconds

What do you think the issue might be?

memory usage

Hello all

We are starting to use apicache in dev and really like it.

Am wondering if anyone has done some memory usage analyses on production environment ? I would be glad to see your results if you have.

Thanks for sharing!

Not respecting hostname

Hello,

I think a server serving multiple virtual hosts should not cache requests from other virtual hosts.
Each host should have a separate cache.

What do you think ?

redis error handling

Hi every once in a while my redis server is unresponsive, which generates the below error. Since its not caught and handled it stops express. Can this be caught in this apicache?

events.js:160
throw er; // Unhandled 'error' event
^

Error: Redis connection to someredisserver.com failed - read ECONNRESET
at exports._errnoException (util.js:1026:11)
at TCP.onread (net.js:569:26)
``

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.