Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions docs/guide/caching/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ store, so a configured hour lived 41.7 days and nothing ever expired.
`maxAge` can be set at three levels, each inheriting from the one above:
`strategy` → content type → route. A route that overrides it wins.

`maxAge: 0` means **do not cache**: the response goes to the client as normal
and nothing is stored, so every request reaches the origin. (Before 5.1.1 a 0
was handed to the store as a lifetime, which it reads as "never expires", so
such a route was cached forever.) Identical requests arriving at the same
moment are still [coalesced](#request-coalescing) into one origin call.

## hitpass

`hitpass` decides, per request, whether to bypass the cache entirely. It is a
Expand Down
10 changes: 10 additions & 0 deletions packages/plugin-rest-cache/server/src/middlewares/recv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,16 @@ export default function createRecv(
// isCacheable has already read whatever Cache-Control the handler set.
setCacheControl(ctx);

// A route with maxAge 0 (or less) is not cached: no entry, and no ETag the
// server could never revalidate. Its Cache-Control, when enabled, already
// says max-age=0.
if (maxAge <= 0) {
debug('strapi:plugin-rest-cache')(
`[RECV] GET ${cacheKey} ${colors.grey('not cached: maxAge 0')}`
);
return;
}

{
const writes: Array<Promise<unknown>> = [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ export default function createCacheStoreService({
async set(key, val, maxAge = 3600000, meta = {}) {
if (unusable()) return null;

// maxAge 0 (or less) means "do not cache". keyv and cache-manager read a
// ttl of 0 as "no expiry", so passing it through stored the entry forever.
if (typeof maxAge === 'number' && maxAge <= 0) return null;

try {
return provider.set(`${keysPrefix}${key}`, serialize(val, meta), maxAge);
} catch (error) {
Expand Down
66 changes: 66 additions & 0 deletions shared/tests/max-age-zero.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use strict";

/**
* `maxAge: 0` means "do not cache".
*
* keyv and cache-manager read a ttl of 0 as "no expiry", so a route configured
* with maxAge 0 was stored forever - while, with cacheControl enabled, the same
* response told clients `max-age=0`. A route that asks for no lifetime gets
* no entry.
*/

const { setup, teardown, agent } = require("./helpers/strapi");

jest.setTimeout(60000);

process.env.STRAPI_DISABLE_UPDATE_NOTIFICATION = true;
process.env.STRAPI_HIDE_STARTUP_MESSAGE = true;
process.env.STRAPI_TELEMETRY_DISABLED = true;

const ROUTE = "/api/categories/probe/inherits";

describe("maxAge: 0", () => {
beforeAll(async () => {
process.env.KEYS_PREFIX = undefined;
// category's maxAge, which its listed /probe/inherits route takes.
process.env.CATEGORY_MAX_AGE = "0";
await setup();
});
afterAll(async () => {
delete process.env.CATEGORY_MAX_AGE;
await teardown();
});

const store = () => strapi.plugin("rest-cache").service("cacheStore");

beforeEach(() => store().reset());

it("does not serve a route with maxAge 0 from the cache", async () => {
const first = await agent().get(ROUTE);
const second = await agent().get(ROUTE);

expect(first.status).toBe(200);
expect(first.get("x-cache")).toBe("MISS");
expect(second.get("x-cache")).toBe("MISS");
});

it("stores nothing for it", async () => {
await agent().get(ROUTE);

const keys = (await store().keys()).filter((k) => k.startsWith(ROUTE));
expect(keys).toEqual([]);
});

it("does not store through cacheStore.set with maxAge 0 either", async () => {
await store().set("/api/direct-zero?", { a: 1 }, 0);

expect(await store().get("/api/direct-zero?")).toBeNull();
});

it("still caches routes with a positive maxAge", async () => {
await agent().get("/api/articles");
const second = await agent().get("/api/articles");

expect(second.get("x-cache")).toBe("HIT");
});
});
Loading