#3311·vinext

kvDataAdapter rejects its own entries for static App Router pages (revalidate: Infinity is stored as null) since 1.0.0-beta.9

Author: heibaCreated Sep 17, 2026Updated Sep 17, 2026

Summary

Since [email protected], a statically generated App Router page is written to the data cache with cacheControl.revalidate: Infinity. kvDataAdapter stores the entry with JSON.stringify, which turns Infinity into null. On the next read, validateCacheEntry requires cacheControl.revalidate to be a number, so the adapter rejects its own entry, logs Invalid cache entry shape, deletes it, and returns a miss.

The page still renders, but every request to it:

  1. reads the KV entry, rejects it and logs console.error
  2. deletes the entry in the background
  3. re-renders and writes the full HTML back to KV

The page is never served from the data cache, and every page view becomes a KV write.

Versions

Package Version
vinext 1.0.0-beta.10 (first bad: 1.0.0-beta.9)
@vinext/cloudflare 1.0.0-beta.8
@cloudflare/vite-plugin 1.54.11
vite 8.3.0
@vitejs/plugin-rsc 0.5.35
react / react-dom 19.3.0
wrangler 4.133.0

Reproduction

A minimal app: one dynamic segment with generateStaticParams, and the KV data adapter.

Files

package.json

json
{
  "name": "vinext-kv-infinity-repro",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "vinext build",
    "preview": "wrangler dev"
  },
  "dependencies": {
    "react": "^19.2.8",
    "react-dom": "^19.2.8",
    "vinext": "1.0.0-beta.10"
  },
  "devDependencies": {
    "@cloudflare/vite-plugin": "^1.52.1",
    "@vinext/cloudflare": "1.0.0-beta.8",
    "@vitejs/plugin-rsc": "^0.5.34",
    "react-server-dom-webpack": "^19.2.8",
    "vite": "^8.2.1",
    "wrangler": "^4.123.0"
  }
}

vite.config.ts

typescript
import { defineConfig } from "vite";
import vinext from "vinext";
import { cloudflare } from "@cloudflare/vite-plugin";
import { kvDataAdapter } from "@vinext/cloudflare/cache/kv-data-adapter";

export default defineConfig({
  plugins: [
    vinext({ cache: { data: kvDataAdapter() } }),
    cloudflare({ viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] } }),
  ],
});

wrangler.jsonc

jsonc
{
  "name": "vinext-kv-infinity-repro",
  "main": "vinext/server/app-router-entry",
  "compatibility_date": "2026-08-03",
  "compatibility_flags": ["nodejs_compat"],
  "assets": { "binding": "ASSETS", "not_found_handling": "none" },
  "kv_namespaces": [{ "binding": "VINEXT_KV_CACHE", "id": "00000000000000000000000000000000" }]
}

app/layout.tsx

typescript
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

app/[lang]/page.tsx

typescript
export function generateStaticParams() {
  return [{ lang: "en" }, { lang: "fr" }];
}

export default async function Page({ params }: { params: Promise<{ lang: string }> }) {
  const { lang } = await params;
  return <h1>Hello ({lang})</h1>;
}

Steps:

bash
bun install && bun run build
wrangler dev
# in another shell, request the same page a few times
curl -s -o /dev/null -D - http://localhost:8787/en   # repeat

Observed

Every request is a miss:

req 1: Cache-Control: no-store, must-revalidate X-Vinext-Cache: MISS
req 2: Cache-Control: no-store, must-revalidate X-Vinext-Cache: MISS
req 3: Cache-Control: no-store, must-revalidate X-Vinext-Cache: MISS
req 4: Cache-Control: no-store, must-revalidate X-Vinext-Cache: MISS

The wrangler dev log shows one error per read after the first write:

✘ [ERROR] [vinext] Invalid cache entry shape for key: app:<build-id>:/en:html

The stored entry, with value omitted:

json
{
  "tags": ["/en", "_N_T_/en", "_N_T_/layout", "_N_T_/[lang]/layout", "_N_T_/[lang]/page"],
  "lastModified": 1789640334484,
  "revalidateAt": null,
  "expireAt": 1821176334484,
  "cacheControl": { "revalidate": null, "expire": 31536000 }
}

set() builds cacheControl only when typeof effectiveRevalidate === "number", so a revalidate: null inside it can only come from a non-finite number serialised by JSON.stringify. revalidateAt is null for the same reason (now + Infinity * 1000).

Expected

The entry round-trips, and the second request is served from the data cache without an error. Before beta.9 these pages were not written to the data cache at all, so the mismatch never surfaced.

Bisect

Same app, wrangler dev, the same page requested four times:

vinext @vinext/cloudflare Page entry written to KV Invalid cache entry shape errors
1.0.0-beta.6 1.0.0-beta.6 no 0
1.0.0-beta.8 1.0.0-beta.6 no 0
1.0.0-beta.9 1.0.0-beta.7 yes 3
1.0.0-beta.10 1.0.0-beta.6 yes 3
1.0.0-beta.10 1.0.0-beta.8 yes 3

The adapter's set() and validateCacheEntry logic is the same in @vinext/cloudflare beta.6 and beta.8. The change in beta.9 is that core now writes these static pages to the data cache, which exposes the round-trip bug. (All rows were bisected on a larger app with the same route shape. Rows 2 and 5 were also confirmed on the minimal app above.)

Where

On current main (ebbebfc):

Possible fix

JSON cannot carry Infinity, so the adapter needs to encode it explicitly. For example: write a non-finite revalidate as null (or a sentinel), accept that value in validateCacheEntry, and restore it to Infinity in get() before returning cacheControl. A round-trip test that runs set() with cacheControl: { revalidate: Infinity } and then get() would catch a regression.

Impact

Besides the log noise, each request to such a page does a KV read, a delete and a put of the full HTML. KV writes are billed, and a key accepts at most one write per second, so a popular page competes with itself.