diff --git a/docs/guide/strategy/index.md b/docs/guide/strategy/index.md index c6d79e029..55200de7f 100644 --- a/docs/guide/strategy/index.md +++ b/docs/guide/strategy/index.md @@ -123,3 +123,19 @@ Specify each content types that should be cached. If a string is provided, defau - **Type:** [`(string|CacheContentTypeConfig)[]`](#cachecontenttypeconfig) - **Default:** `[]` + +### `cacheControlHeaders` +Configure how to treat `Cache-Control` header in requests and responses. +If omitted (default) - ignore `Cache-Control` header in requests and do not add it to responses. + +- **Type:** `CacheControlHeaderConfig` +- **Default:** `{}` +- **Example**: +```js +{ + response: { + staleWhileRevalidate: 3600, + maxAge: 'CONFIG', + } +} +``` diff --git a/packages/strapi-plugin-rest-cache/server/config/index.js b/packages/strapi-plugin-rest-cache/server/config/index.js index 64c4d6614..155489eb5 100644 --- a/packages/strapi-plugin-rest-cache/server/config/index.js +++ b/packages/strapi-plugin-rest-cache/server/config/index.js @@ -25,6 +25,7 @@ const config = { ), maxAge: 3600000, contentTypes: [], + cacheControlHeader: {}, }, }), validator() {}, diff --git a/packages/strapi-plugin-rest-cache/server/middlewares/recv.js b/packages/strapi-plugin-rest-cache/server/middlewares/recv.js index dbf34dc93..7bbdca26c 100644 --- a/packages/strapi-plugin-rest-cache/server/middlewares/recv.js +++ b/packages/strapi-plugin-rest-cache/server/middlewares/recv.js @@ -12,6 +12,7 @@ const { shouldLookup } = require('../utils/middlewares/shouldLookup'); const { etagGenerate } = require('../utils/etags/etagGenerate'); const { etagLookup } = require('../utils/etags/etagLookup'); const { etagMatch } = require('../utils/etags/etagMatch'); +const { setCacheControlHeader } = require('../utils/cache-control/cacheControlResponse'); /** * @param {{ cacheRouteConfig: CacheRouteConfig }} options @@ -27,7 +28,7 @@ function createRecv(options, { strapi }) { const { strategy } = strapi.config.get('plugin.rest-cache'); const { cacheRouteConfig } = options; const { hitpass, maxAge, keys } = cacheRouteConfig; - const { enableEtag = false, enableXCacheHeaders = false } = strategy; + const { enableEtag = false, enableXCacheHeaders = false, cacheControlHeader = null } = strategy; return async function recv(ctx, next) { // hash @@ -73,6 +74,8 @@ function createRecv(options, { strapi }) { ctx.set('ETag', `"${etagCached}"`); } + setCacheControlHeader(ctx, cacheControlHeader, cacheRouteConfig); + ctx.status = 200; ctx.body = cacheEntry; return; @@ -102,7 +105,7 @@ function createRecv(options, { strapi }) { } if (ctx.body && ctx.status >= 200 && ctx.status <= 300) { - // @TODO check Cache-Control response header + setCacheControlHeader(ctx, cacheControlHeader, cacheRouteConfig); if (enableEtag) { const etag = etagGenerate(ctx, cacheKey); diff --git a/packages/strapi-plugin-rest-cache/server/types/CacheControlHeaderConfig.js b/packages/strapi-plugin-rest-cache/server/types/CacheControlHeaderConfig.js new file mode 100644 index 000000000..5defaea4f --- /dev/null +++ b/packages/strapi-plugin-rest-cache/server/types/CacheControlHeaderConfig.js @@ -0,0 +1,22 @@ +'use strict'; + +const { CacheControlResponseHeaderConfig } = require("./CacheControlResponseHeaderConfig"); + + +class CacheControlHeaderConfig { + + /** + * @type {CacheControlResponseHeaderConfig} + */ + response = null; + + constructor(options = {}) { + const { + response = null, + } = options; + + this.response = response; + } +} + +module.exports = { CacheControlHeaderConfig }; \ No newline at end of file diff --git a/packages/strapi-plugin-rest-cache/server/types/CacheControlResponseHeaderConfig.js b/packages/strapi-plugin-rest-cache/server/types/CacheControlResponseHeaderConfig.js new file mode 100644 index 000000000..b2b1f3e9d --- /dev/null +++ b/packages/strapi-plugin-rest-cache/server/types/CacheControlResponseHeaderConfig.js @@ -0,0 +1,41 @@ +const { CacheContentTypeConfig } = require("./CacheContentTypeConfig"); + +const CacheControlResponseMaxAge = Object.freeze({ + /** + * Do not include maxAge in response Cache-Control + */ + NONE: "NONE", + /** + * Include maxAge in response Cache-Control - set it to maxAge configured in Strategy + */ + CONFIG: "CONFIG", +}); + +class CacheControlResponseHeaderConfig { + /** + * stale-while-revalidate + * see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives + * @type {number | boolean} + */ + staleWhileRevalidate = false; + + /** + * @type {CacheControlResponseMaxAge | number} + */ + maxAge = CacheControlResponseMaxAge.NONE; + + constructor(options = {}) { + const { + staleWhileRevalidate = false, + maxAge = CacheControlResponseMaxAge.NONE, + } = options; + + this.staleWhileRevalidate = staleWhileRevalidate; + this.maxAge = maxAge; + } +} + +module.exports = { + CacheControlResponseHeaderConfig, + CacheControlResponseMaxAge, +} diff --git a/packages/strapi-plugin-rest-cache/server/types/CachePluginStrategy.js b/packages/strapi-plugin-rest-cache/server/types/CachePluginStrategy.js index 3cf47d376..410875153 100644 --- a/packages/strapi-plugin-rest-cache/server/types/CachePluginStrategy.js +++ b/packages/strapi-plugin-rest-cache/server/types/CachePluginStrategy.js @@ -1,7 +1,9 @@ 'use strict'; +const { CacheControlHeaderConfig } = require('./CacheControlHeaderConfig'); /** * @typedef {import('./CacheContentTypeConfig').CacheContentTypeConfig} CacheContentTypeConfig + * @typedef {import('./CacheControlHeaderConfig').CacheControlHeaderConfig} CacheControlHeaderConfig */ const { CacheKeysConfig } = require('./CacheKeysConfig'); @@ -32,6 +34,11 @@ class CachePluginStrategy { */ keys; + /** + * @param {CacheControlHeaderConfig} + */ + cacheControlHeader; + constructor(options = {}) { const { debug = false, @@ -44,6 +51,7 @@ class CachePluginStrategy { keysPrefix = '', contentTypes = [], keys = new CacheKeysConfig(), + cacheControlHeader = new CacheControlHeaderConfig(), } = options; this.debug = debug; @@ -56,6 +64,7 @@ class CachePluginStrategy { this.keysPrefix = keysPrefix; this.contentTypes = contentTypes; this.keys = keys; + this.cacheControlHeader = cacheControlHeader; } } diff --git a/packages/strapi-plugin-rest-cache/server/types/index.js b/packages/strapi-plugin-rest-cache/server/types/index.js index 643a56a1b..2853407e7 100644 --- a/packages/strapi-plugin-rest-cache/server/types/index.js +++ b/packages/strapi-plugin-rest-cache/server/types/index.js @@ -5,6 +5,8 @@ const { CacheRouteConfig } = require('./CacheRouteConfig'); const { CacheProvider } = require('./CacheProvider'); const { CacheContentTypeConfig } = require('./CacheContentTypeConfig'); const { CacheKeysConfig } = require('./CacheKeysConfig'); +const { CacheControlHeaderConfig: CacheControlHeaderConfig } = require('./CacheControlHeaderConfig'); +const { CacheControlResponseHeaderConfig } = require('./CacheControlResponseHeaderConfig'); module.exports = { CachePluginStrategy, @@ -12,4 +14,6 @@ module.exports = { CacheProvider, CacheContentTypeConfig, CacheKeysConfig, + CacheControlHeaderConfig, + CacheControlResponseHeaderConfig, }; diff --git a/packages/strapi-plugin-rest-cache/server/utils/cache-control/cacheControlResponse.js b/packages/strapi-plugin-rest-cache/server/utils/cache-control/cacheControlResponse.js new file mode 100644 index 000000000..891b6b730 --- /dev/null +++ b/packages/strapi-plugin-rest-cache/server/utils/cache-control/cacheControlResponse.js @@ -0,0 +1,92 @@ +'use strict'; + +const { CacheControlHeaderConfig, CacheRouteConfig } = require("../../types"); +const { CacheControlResponseMaxAge } = require("../../types/CacheControlResponseHeaderConfig"); +const debug = require('debug')('strapi:strapi-plugin-rest-cache'); + + +/** + * + * @param {*} ctx + * @param {CacheControlHeaderConfig} cacheControlHeaderConfig + * @param {CacheRouteConfig} cacheRouteConfig + * @returns + */ +function setCacheControlHeader(ctx, cacheControlHeaderConfig, cacheRouteConfig) { + const responseConfig = cacheControlHeaderConfig?.response; + if (!responseConfig) { + return; + } + + const { hitpass, maxAge, keys } = cacheRouteConfig; + + let cacheControlHeader = ctx.response.get('Cache-Control'); + + const directivesToSet = new Map(); + + const staleWhileRevalidateSeconds = responseConfig.staleWhileRevalidate; + if (staleWhileRevalidateSeconds) { + directivesToSet.set("stale-while-revalidate", staleWhileRevalidateSeconds); + } + + if (maxAge && responseConfig.maxAge == CacheControlResponseMaxAge.CONFIG) { + directivesToSet.set("max-age", maxAge); + } + if (Number.isInteger(responseConfig.maxAge)) { + directivesToSet.set("max-age", responseConfig.maxAge); + } + + debug("cacheControlHeader before:" + cacheControlHeader); + const newCacheControlHeader = setNewCacheControlDirectives(cacheControlHeader, directivesToSet); + debug("cacheControlHeader after:" + newCacheControlHeader); + + ctx.response.set('Cache-Control', newCacheControlHeader); +} + +/** + * set directive, without overriding existing ones + */ +function setNewCacheControlDirectives(cacheControlHeader, directivesToSet) { + + if (!directivesToSet || directivesToSet.length == 0) { + return cacheControlHeader; + } + if (!cacheControlHeader) { + cacheControlHeader = ""; + } + + // Split the existing Cache-Control header by commas to get individual directives + const currentDirectivesArray = cacheControlHeader.split(',').map(d => d.trim()).filter(d => d != ""); + + // Create a map to store current directives and their values (if any) + const currentDirectives = new Map(); + + // Parse the existing directives and store them in the map + currentDirectivesArray.forEach(directive => { + const [key, value] = directive.split('=').map(part => part.trim()); + currentDirectives.set(key, value || null); // If there's no value, set it to null + }); + + directivesToSet.forEach((value, key) => { + // Only add the directive if it's not already set + if (!currentDirectives.has(key)) { + currentDirectives.set(key, value); + } + }); + + // Convert the updated map back into a string + const updatedDirectives = []; + currentDirectives.forEach((value, key) => { + if (value !== null) { + updatedDirectives.push(`${key}=${value}`); + } else { + updatedDirectives.push(key); + } + }); + + return updatedDirectives.join(', '); +} + +module.exports = { + setCacheControlHeader, +};