Repository: vercel/next.js
Pull request: fix: ensure LRU cache items have minimum size of 1 to prevent unbounded growth

### What?

Prevent LRU cache unbounded growth by requiring `calculateSize` to return values > 0.

### Why?

When `calculateSize` returned 0 (e.g., for `null` values in the filesystem route cache), items were never evicted because they didn't contribute to `totalSize`. This caused unbounded memory growth when handling dynamic routes like `/api/logs/[id]` where each unique ID created a new cache entry with size 0.

### How?

1. **LRUCache now throws an error if size ≤ 0** - This catches bugs at development time rather than silently masking them:
   ```typescript
   if (size <= 0) {
     throw new Error(
       \`LRUCache: calculateSize returned ${size}, but size must be > 0. \` +
       \`Items with size 0 would never be evicted, causing unbounded cache growth.\`
     )
   }
   ```

2. **Fixed filesystem.ts** - Null entries (negative cache) now return size 1 instead of 0:
   ```typescript
   if (!value) {
     // Null entries (negative cache) still need a non-zero size for LRU eviction
     return 1
   }
   ```

3. **Fixed next-dev-server.ts** - Static paths cache now uses `|| 1` instead of `?? 0`:
   ```typescript
   return JSON.stringify(value.staticPaths)?.length || 1
   ```

Fixes #89033

Changed files:
filename: packages/next/errors.json
patch: @@ -981,5 +981,6 @@
   "980": "Failed to load client middleware manifest",
   "981": "resolvedPathname must be set in request metadata",
   "982": "`serializeResumeDataCache` should not be called in edge runtime.",
-  "983": "Invariant: global-error module is required but not found in loader tree"
+  "983": "Invariant: global-error module is required but not found in loader tree",
+  "984": "LRUCache: calculateSize returned %s, but size must be > 0. Items with size 0 would never be evicted, causing unbounded cache growth."
 }

filename: packages/next/src/server/dev/next-dev-server.ts
patch: @@ -186,7 +186,8 @@ export default class DevServer extends Server {
       // 5MB
       5 * 1024 * 1024,
       function length(value) {
-        return JSON.stringify(value.staticPaths)?.length ?? 0
+        // Ensure minimum size of 1 for LRU eviction to work correctly
+        return JSON.stringify(value.staticPaths)?.length || 1
       }
     )
 

filename: packages/next/src/server/lib/lru-cache.ts
patch: @@ -125,6 +125,12 @@ export class LRUCache<T> {
    */
   public set(key: string, value: T): void {
     const size = this.calculateSize?.(value) ?? 1
+    if (size <= 0) {
+      throw new Error(
+        `LRUCache: calculateSize returned ${size}, but size must be > 0. ` +
+          `Items with size 0 would never be evicted, causing unbounded cache growth.`
+      )
+    }
     if (size > this.maxSize) {
       console.warn('Single item size exceeds maxSize')
       return

filename: packages/next/src/server/lib/router-utils/filesystem.ts
patch: @@ -113,7 +113,10 @@ export async function setupFsCheck(opts: {
 }) {
   const getItemsLru = !opts.dev
     ? new LRUCache<FsOutput | null>(1024 * 1024, function length(value) {
-        if (!value) return 0
+        if (!value) {
+          // Null entries (negative cache) still need a non-zero size for LRU eviction
+          return 1
+        }
         return (
           (value.fsPath || '').length +
           value.itemPath.length +