diff --git a/packages/cloudflare/src/cache/kv-data-adapter.runtime.ts b/packages/cloudflare/src/cache/kv-data-adapter.runtime.ts index bf01363b8e..934568e6ad 100644 --- a/packages/cloudflare/src/cache/kv-data-adapter.runtime.ts +++ b/packages/cloudflare/src/cache/kv-data-adapter.runtime.ts @@ -469,6 +469,13 @@ export class KVCacheHandler implements CacheHandler { effectiveRevalidate = data.revalidate; } if (effectiveRevalidate === 0) return Promise.resolve(); + if ( + typeof effectiveRevalidate === "number" && + !Number.isFinite(effectiveRevalidate) && + effectiveRevalidate !== Infinity + ) { + return Promise.resolve(); + } const now = Date.now(); const revalidateAt = @@ -684,7 +691,11 @@ function validateCacheEntry(raw: unknown): KVCacheEntry | null { } if (obj.cacheControl !== undefined) { if (!isUnknownRecord(obj.cacheControl)) return null; - if (typeof obj.cacheControl.revalidate !== "number") return null; + // `null` is the JSON encoding of `Infinity` used by static pages. + if (typeof obj.cacheControl.revalidate !== "number" && obj.cacheControl.revalidate !== null) { + return null; + } + if (obj.cacheControl.revalidate === null) obj.cacheControl.revalidate = Infinity; if (obj.cacheControl.expire !== undefined && typeof obj.cacheControl.expire !== "number") { return null; } diff --git a/tests/kv-cache-handler.test.ts b/tests/kv-cache-handler.test.ts index bbe18fa08e..6b58e97224 100644 --- a/tests/kv-cache-handler.test.ts +++ b/tests/kv-cache-handler.test.ts @@ -639,6 +639,44 @@ describe("KVCacheHandler", () => { expect(hit?.cacheControl).toEqual({ revalidate: 60, expire: 300, stale: 30 }); }); + it("round-trips the Infinity revalidate used by static pages", async () => { + await handler.set( + "static-infinity", + { + kind: "APP_PAGE", + html: "
static
", + rscData: undefined, + headers: undefined, + postponed: undefined, + status: 200, + }, + { cacheControl: { revalidate: Infinity } }, + ); + + const stored = JSON.parse(store.get("cache:static-infinity")!); + expect(stored).toMatchObject({ + revalidateAt: null, + cacheControl: { revalidate: null }, + }); + expect(kv.put).toHaveBeenCalledWith("cache:static-infinity", expect.any(String), { + expirationTtl: 30 * 24 * 3600, + metadata: { tags: [] }, + }); + expect((await handler.get("static-infinity"))?.cacheControl).toEqual({ + revalidate: Infinity, + }); + expect(kv.delete).not.toHaveBeenCalled(); + }); + + it.each([NaN, -Infinity])( + "does not encode invalid revalidate %s as Infinity", + async (value) => { + await handler.set("invalid-revalidate", null, { cacheControl: { revalidate: value } }); + + expect(kv.put).not.toHaveBeenCalled(); + }, + ); + it("serves stale when a shorter read-time revalidate has elapsed", async () => { vi.useFakeTimers({ toFake: ["Date"] }); vi.setSystemTime(1_000);