├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── index.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [2, 2], 4 | "generator-star-spacing": [2, "after"] 5 | }, 6 | "env": { 7 | "es6": true, 8 | "node": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - "6" 3 | - "7" 4 | language: node_js 5 | script: "npm run coverage" 6 | after_script: "npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Christopher Sidebottom 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # koa-cache-control 2 | 3 | A simple method for managing cache control headers from your application. It also tries to provide a simple set of rules for common use cases such as setting 'max-age=0' when 'no-cache' is present by default. 4 | 5 | ## Example 6 | Configuring noCache easily: 7 | ```js 8 | app.use(cacheControl({ 9 | noCache: true 10 | })); 11 | ``` 12 | Creates a cache-control header of `no-cache, max-age=0` 13 | 14 | ## Usage 15 | To start using cacheControl, just use the middleware in your application: 16 | ```js 17 | app.use(cacheControl()); 18 | ``` 19 | 20 | ### Default Cache Headers 21 | When initialising the middleware you can set default options when you use it in your application: 22 | ```js 23 | app.use(cacheControl({ 24 | maxAge: 5 25 | })); 26 | ``` 27 | 28 | ### Overriding Defaults 29 | Just set the cacheControl object after the cacheControl() middleware is loaded on the request context: 30 | ```js 31 | app.use(function (ctx, next) { 32 | ctx.cacheControl = { 33 | maxAge: 60 34 | }; 35 | 36 | return next(); 37 | }); 38 | ``` 39 | 40 | This is useful in error conditions where you can setup cache headers before and after a request is processed: 41 | ```js 42 | app.use(function (ctx, next) { 43 | ctx.cacheControl = { 44 | maxAge: 60 45 | }; 46 | 47 | return next().catch((err) => { 48 | ctx.cacheControl = { 49 | maxAge: 5 50 | }; 51 | }); 52 | }); 53 | ``` 54 | 55 | ## Options 56 | Name | Value | Description 57 | ----|----|--- 58 | private | Boolean | Adds 'private' flag, overrides 'public' option 59 | public | Boolean | Adds 'public' flag 60 | noStore | Boolean | Adds 'no-store' flag and includes noCache 61 | noCache | Boolean | Adds 'no-cache' flag, sets maxAge to 0 and removes sMaxAge, staleIfError and staleWhileRevalidate 62 | noTransform | Boolean | Adds 'no-transform' flag 63 | mustRevalidate | Boolean | Adds 'must-revalidate' flag and removes staleIfError and staleWhileRevalidate 64 | staleIfError | Number | Adds 'stale-if-error=%d' flag 65 | staleWhileRevalidate | Number | Adds 'stale-while-revalidate=%d' flag 66 | maxAge | Number | Adds 'max-age=%d' flag 67 | sMaxAge | Number | Adds 's-maxage=%d' flag 68 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (defaults) { 2 | return function cacheControl(ctx, next) { 3 | return next().then(function () { 4 | var options = ctx.cacheControl || defaults || {}; 5 | var cacheControl = []; 6 | 7 | if (options.private) { 8 | cacheControl.push('private'); 9 | } else if (options.public) { 10 | cacheControl.push('public'); 11 | } 12 | 13 | if (options.noStore) { 14 | options.noCache = true; 15 | cacheControl.push('no-store'); 16 | } 17 | 18 | if (options.noCache) { 19 | options.maxAge = 0; 20 | delete options.sMaxAge; 21 | cacheControl.push('no-cache'); 22 | } 23 | 24 | if (options.noTransform) { 25 | cacheControl.push('no-transform'); 26 | } 27 | 28 | if (options.proxyRevalidate) { 29 | cacheControl.push('proxy-revalidate'); 30 | } 31 | 32 | if (options.mustRevalidate) { 33 | cacheControl.push('must-revalidate'); 34 | } else if (!options.noCache) { 35 | if (options.staleIfError) { 36 | cacheControl.push(`stale-if-error=${options.staleIfError}`); 37 | } 38 | 39 | if (options.staleWhileRevalidate) { 40 | cacheControl.push(`stale-while-revalidate=${options.staleWhileRevalidate}`); 41 | } 42 | } 43 | 44 | if (Number.isInteger(options.maxAge)) { 45 | cacheControl.push(`max-age=${options.maxAge}`); 46 | } 47 | 48 | if (Number.isInteger(options.sMaxAge)) { 49 | cacheControl.push(`s-maxage=${options.sMaxAge}`); 50 | } 51 | 52 | if (cacheControl.length) { 53 | ctx.set('Cache-Control', cacheControl.join(',')); 54 | } 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-cache-control", 3 | "version": "2.0.0", 4 | "description": "Middleware for meddling with Cache-Control headers", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "lint": "eslint .", 9 | "coverage": "istanbul cover _mocha -- -R dot" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/DaMouse404/koa-cache-control" 14 | }, 15 | "keywords": [ 16 | "Koa", 17 | "cache", 18 | "cache-control", 19 | "no-cache" 20 | ], 21 | "author": "Christopher Sidebottom", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/DaMouse404/koa-cache-headers/issues" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^6.6.0", 28 | "istanbul": "^0.4.3", 29 | "koa": "^2.0.0", 30 | "mocha": "^3.0.2", 31 | "supertest": "^3.0.0" 32 | }, 33 | "dependencies": { 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const koa = require('koa'); 3 | const cacheControl = require('..'); 4 | const fs = require('fs'); 5 | 6 | describe('cacheControl()', function () { 7 | describe('default configuration', function () { 8 | it('uses defaults if nothing defined on request', function (done) { 9 | const app = new koa(); 10 | 11 | app.use(cacheControl({ 12 | maxAge: 4 13 | })); 14 | 15 | app.use(function (ctx, next) { 16 | return next(); 17 | }); 18 | 19 | request(app.listen()) 20 | .get('/') 21 | .expect('Cache-Control', 'max-age=4') 22 | .end(done); 23 | }) 24 | }); 25 | 26 | describe('override default configuration', function () { 27 | it('allows middleware to override options in incoming requests', function (done) { 28 | const app = new koa(); 29 | 30 | app.use(cacheControl({ 31 | maxAge: 4 32 | })); 33 | 34 | app.use(function (ctx, next) { 35 | ctx.cacheControl = { 36 | maxAge: 60 37 | }; 38 | 39 | return next(); 40 | }); 41 | 42 | request(app.listen()) 43 | .get('/') 44 | .expect('Cache-Control', 'max-age=60') 45 | .end(done); 46 | }) 47 | 48 | it('allows middleware to override options on outgoing requests', function (done) { 49 | const app = new koa(); 50 | 51 | app.use(cacheControl({ 52 | maxAge: 300 53 | })); 54 | 55 | app.use(function (ctx, next) { 56 | return next().catch(function (err) { 57 | ctx.cacheControl = { 58 | noCache: true 59 | }; 60 | }); 61 | }); 62 | 63 | app.use(function (ctx, next) { 64 | ctx.throw(500); 65 | }); 66 | 67 | request(app.listen()) 68 | .get('/') 69 | .expect('Cache-Control', 'no-cache,max-age=0') 70 | .end(done); 71 | }) 72 | }); 73 | 74 | describe('public is set', function () { 75 | it('adds public flag to cache-control header', function (done) { 76 | const app = new koa(); 77 | 78 | app.use(cacheControl({ 79 | public: true 80 | })); 81 | 82 | app.use(function (ctx, next) { 83 | return next(); 84 | }); 85 | 86 | request(app.listen()) 87 | .get('/') 88 | .expect('Cache-Control', 'public') 89 | .end(done); 90 | }); 91 | }); 92 | 93 | describe('private is set', function () { 94 | it('adds private flag to cache-control header', function (done) { 95 | const app = new koa(); 96 | 97 | app.use(cacheControl({ 98 | private: true 99 | })); 100 | 101 | app.use(function (ctx, next) { 102 | return next(); 103 | }); 104 | 105 | request(app.listen()) 106 | .get('/') 107 | .expect('Cache-Control', 'private') 108 | .end(done); 109 | }); 110 | 111 | it('discards public flag in cache-control header', function (done) { 112 | const app = new koa(); 113 | 114 | app.use(cacheControl({ 115 | private: true, 116 | public: true 117 | })); 118 | 119 | app.use(function (ctx, next) { 120 | return next(); 121 | }); 122 | 123 | request(app.listen()) 124 | .get('/') 125 | .expect('Cache-Control', 'private') 126 | .end(done); 127 | }); 128 | }); 129 | 130 | describe('maxAge is set', function () { 131 | it('sets cache-control max-age section', function (done) { 132 | const app = new koa(); 133 | 134 | app.use(cacheControl({ 135 | maxAge: 4 136 | })); 137 | 138 | app.use(function (ctx, next) { 139 | return next(); 140 | }); 141 | 142 | request(app.listen()) 143 | .get('/') 144 | .expect('Cache-Control', 'max-age=4') 145 | .end(done); 146 | }); 147 | }); 148 | 149 | describe('staleIfError is set', function () { 150 | it('sets cache-control header with stale-if-error', function (done) { 151 | const app = new koa(); 152 | 153 | app.use(cacheControl({ 154 | staleIfError: 320 155 | })); 156 | 157 | app.use(function (ctx, next) { 158 | return next(); 159 | }); 160 | 161 | request(app.listen()) 162 | .get('/') 163 | .expect('Cache-Control', 'stale-if-error=320') 164 | .end(done); 165 | }); 166 | }); 167 | 168 | describe('staleWhileRevalidate is set', function () { 169 | it('sets cache-control header with stale-while-revalidate', function (done) { 170 | const app = new koa(); 171 | 172 | app.use(cacheControl({ 173 | staleWhileRevalidate: 320 174 | })); 175 | 176 | app.use(function (ctx, next) { 177 | return next(); 178 | }); 179 | 180 | request(app.listen()) 181 | .get('/') 182 | .expect('Cache-Control', 'stale-while-revalidate=320') 183 | .end(done); 184 | }); 185 | }); 186 | 187 | describe('mustRevalidate is set', function () { 188 | it('sets cache-control header with must-revalidate', function (done) { 189 | const app = new koa(); 190 | 191 | app.use(cacheControl({ 192 | mustRevalidate: true 193 | })); 194 | 195 | app.use(function (ctx, next) { 196 | return next(); 197 | }); 198 | 199 | request(app.listen()) 200 | .get('/') 201 | .expect('Cache-Control', 'must-revalidate') 202 | .end(done); 203 | }); 204 | 205 | it('overthrows stale-while-revalidate and stale-if-error', function (done) { 206 | const app = new koa(); 207 | 208 | app.use(cacheControl({ 209 | mustRevalidate: true, 210 | staleWhileRevalidate: 320, 211 | staleIfError: 404 212 | })); 213 | 214 | app.use(function (ctx, next) { 215 | return next(); 216 | }); 217 | 218 | request(app.listen()) 219 | .get('/') 220 | .expect('Cache-Control', 'must-revalidate') 221 | .end(done); 222 | }); 223 | }); 224 | 225 | describe('when noCache is true', function () { 226 | it('adds no-cache to Cache-Control header', function (done) { 227 | const app = new koa(); 228 | 229 | app.use(cacheControl({ 230 | noCache: true 231 | })); 232 | 233 | app.use(function (ctx, next) { 234 | return next(); 235 | }); 236 | 237 | request(app.listen()) 238 | .get('/') 239 | .expect('Cache-Control', 'no-cache,max-age=0') 240 | .end(done); 241 | }); 242 | 243 | it('sets maxAge to 0', function (done) { 244 | const app = new koa(); 245 | 246 | app.use(cacheControl({ 247 | noCache: true, 248 | maxAge: 60 249 | })); 250 | 251 | app.use(function (ctx, next) { 252 | return next(); 253 | }); 254 | 255 | request(app.listen()) 256 | .get('/') 257 | .expect('Cache-Control', 'no-cache,max-age=0') 258 | .end(done); 259 | }); 260 | 261 | it('removes sMaxAge', function (done) { 262 | const app = new koa(); 263 | 264 | app.use(cacheControl({ 265 | noCache: true, 266 | sMaxAge: 60 267 | })); 268 | 269 | app.use(function (ctx, next) { 270 | return next(); 271 | }); 272 | 273 | request(app.listen()) 274 | .get('/') 275 | .expect('Cache-Control', 'no-cache,max-age=0') 276 | .end(done); 277 | }); 278 | 279 | it('ignores stale settings', function (done) { 280 | const app = new koa(); 281 | 282 | app.use(cacheControl({ 283 | noCache: true, 284 | staleIfError: 404, 285 | staleWhileRevalidate: 10 286 | })); 287 | 288 | app.use(function (ctx, next) { 289 | return next(); 290 | }); 291 | 292 | request(app.listen()) 293 | .get('/') 294 | .expect('Cache-Control', 'no-cache,max-age=0') 295 | .end(done); 296 | }); 297 | }); 298 | 299 | describe('when noStore is true', function () { 300 | it('sets Cache-Control no-store and no-cache', function (done) { 301 | const app = new koa(); 302 | 303 | app.use(cacheControl({ 304 | noStore: true 305 | })); 306 | 307 | app.use(function (ctx, next) { 308 | return next(); 309 | }); 310 | 311 | request(app.listen()) 312 | .get('/') 313 | .expect('Cache-Control', 'no-store,no-cache,max-age=0') 314 | .end(done); 315 | }); 316 | 317 | it('sets maxAge to 0', function (done) { 318 | const app = new koa(); 319 | 320 | app.use(cacheControl({ 321 | noStore: true, 322 | maxAge: 50 323 | })); 324 | 325 | app.use(function (ctx, next) { 326 | return next(); 327 | }); 328 | 329 | request(app.listen()) 330 | .get('/') 331 | .expect('Cache-Control', 'no-store,no-cache,max-age=0') 332 | .end(done); 333 | }); 334 | }); 335 | 336 | describe('when noTransform is set', function () { 337 | it('sets Cache-Control no-transform', function (done) { 338 | const app = new koa(); 339 | 340 | app.use(cacheControl({ 341 | noTransform: true 342 | })); 343 | 344 | app.use(function (ctx, next) { 345 | return next(); 346 | }); 347 | 348 | request(app.listen()) 349 | .get('/') 350 | .expect('Cache-Control', 'no-transform') 351 | .end(done); 352 | }); 353 | }); 354 | 355 | describe('when proxyRevalidate', function () { 356 | it('sets Cache-Control proxy-revalidate', function (done) { 357 | const app = new koa(); 358 | 359 | app.use(cacheControl({ 360 | proxyRevalidate: true 361 | })); 362 | 363 | app.use(function (ctx, next) { 364 | return next(); 365 | }); 366 | 367 | request(app.listen()) 368 | .get('/') 369 | .expect('Cache-Control', 'proxy-revalidate') 370 | .end(done); 371 | }); 372 | }); 373 | 374 | 375 | describe('when sMaxAge', function () { 376 | it('sets Cache-Control s-maxage property', function (done) { 377 | const app = new koa(); 378 | 379 | app.use(cacheControl({ 380 | sMaxAge: 10 381 | })); 382 | 383 | app.use(function (ctx, next) { 384 | return next(); 385 | }); 386 | 387 | request(app.listen()) 388 | .get('/') 389 | .expect('Cache-Control', 's-maxage=10') 390 | .end(done); 391 | }); 392 | }); 393 | 394 | describe('when no cache properties set', function () { 395 | it('does not set a cache-control header', function (done) { 396 | const app = new koa(); 397 | 398 | app.use(cacheControl()); 399 | 400 | app.use(function (ctx, next) { 401 | return next(); 402 | }); 403 | 404 | request(app.listen()) 405 | .get('/') 406 | .expect(function (res) { 407 | return res.headers['Cache-Control'] === undefined; 408 | }) 409 | .end(done); 410 | }); 411 | }); 412 | }); 413 | --------------------------------------------------------------------------------