From ab26e187d7a260ac57c1e9451fdd8bfb2fb59160 Mon Sep 17 00:00:00 2001 From: "vercel-gh-bot-3[bot]" <282332853+vercel-gh-bot-3[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 21:29:07 +0000 Subject: [PATCH 1/2] test: cover filesystem cache URL key accounting --- .../lib/router-utils/filesystem.test.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 packages/next/src/server/lib/router-utils/filesystem.test.ts diff --git a/packages/next/src/server/lib/router-utils/filesystem.test.ts b/packages/next/src/server/lib/router-utils/filesystem.test.ts new file mode 100644 index 00000000000..39e6a5433a9 --- /dev/null +++ b/packages/next/src/server/lib/router-utils/filesystem.test.ts @@ -0,0 +1,73 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises' +import { tmpdir } from 'os' +import { join } from 'path' +import { defaultConfig } from '../../config-shared' +import { setupFsCheck } from './filesystem' + +describe('filesystem cache', () => { + let dir: string + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), 'next-fs-check-')) + const distDir = join(dir, '.next') + const serverDir = join(distDir, 'server') + await mkdir(serverDir, { recursive: true }) + + await Promise.all([ + writeFile(join(distDir, 'BUILD_ID'), 'build-id'), + writeFile( + join(distDir, 'routes-manifest.json'), + JSON.stringify({ + redirects: [], + rewrites: { beforeFiles: [], afterFiles: [], fallback: [] }, + headers: [], + onMatchHeaders: [], + dynamicRoutes: [], + dataRoutes: [], + }) + ), + writeFile( + join(distDir, 'prerender-manifest.json'), + JSON.stringify({ + preview: { + previewModeId: 'id', + previewModeSigningKey: 'signing-key', + previewModeEncryptionKey: 'encryption-key', + }, + }) + ), + writeFile(join(serverDir, 'pages-manifest.json'), '{}'), + writeFile( + join(serverDir, 'functions-config-manifest.json'), + JSON.stringify({ functions: {} }) + ), + ]) + }) + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }) + }) + + it('includes URL keys when limiting the production filesystem cache', async () => { + const fsChecker = await setupFsCheck({ + dir, + dev: false, + config: { ...defaultConfig, output: 'standalone' }, + }) + + fsChecker.pageFiles.add('/first') + expect(await fsChecker.getItem('/first')).toMatchObject({ + itemPath: '/first', + type: 'pageFile', + }) + fsChecker.pageFiles.delete('/first') + + // The retained URL keys together exceed the cache's 1 MiB size limit. + const longSegment = 'x'.repeat(1024) + for (let index = 0; index < 1100; index++) { + await fsChecker.getItem(`/${longSegment}/${index}`) + } + + expect(await fsChecker.getItem('/first')).toBeNull() + }) +}) From 1895c0d02e5a1a7893259c017e71b71b8be648fb Mon Sep 17 00:00:00 2001 From: "vercel-gh-bot-3[bot]" <282332853+vercel-gh-bot-3[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 21:38:37 +0000 Subject: [PATCH 2/2] fix: include keys in filesystem cache size --- packages/next/src/server/lib/lru-cache.ts | 8 +++++--- packages/next/src/server/lib/router-utils/filesystem.ts | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/next/src/server/lib/lru-cache.ts b/packages/next/src/server/lib/lru-cache.ts index 59b76b5d0b4..59c97293c0e 100644 --- a/packages/next/src/server/lib/lru-cache.ts +++ b/packages/next/src/server/lib/lru-cache.ts @@ -49,12 +49,14 @@ export class LRUCache { private readonly tail: SentinelNode private totalSize: number = 0 private readonly maxSize: number - private readonly calculateSize: ((value: T) => number) | undefined + private readonly calculateSize: + | ((value: T, key: string) => number) + | undefined private readonly onEvict: ((key: string, value: T) => void) | undefined constructor( maxSize: number, - calculateSize?: (value: T) => number, + calculateSize?: (value: T, key: string) => number, onEvict?: (key: string, value: T) => void ) { this.maxSize = maxSize @@ -124,7 +126,7 @@ export class LRUCache { * - O(k) where k is the number of items evicted (can be O(N) for variable sizes) */ public set(key: string, value: T): boolean { - const size = this.calculateSize?.(value) ?? 1 + const size = this.calculateSize?.(value, key) ?? 1 if (size <= 0) { throw new Error( `LRUCache: calculateSize returned ${size}, but size must be > 0. ` + diff --git a/packages/next/src/server/lib/router-utils/filesystem.ts b/packages/next/src/server/lib/router-utils/filesystem.ts index 6909eb2c773..eef4c50ef04 100644 --- a/packages/next/src/server/lib/router-utils/filesystem.ts +++ b/packages/next/src/server/lib/router-utils/filesystem.ts @@ -113,12 +113,14 @@ export async function setupFsCheck(opts: { config: NextConfigRuntime }) { const getItemsLru = !opts.dev - ? new LRUCache(1024 * 1024, function length(value) { + ? new LRUCache(1024 * 1024, function length(value, key) { + const keyLength = key.length if (!value) { // Null entries (negative cache) still need a non-zero size for LRU eviction - return 1 + return keyLength || 1 } return ( + keyLength + (value.fsPath || '').length + value.itemPath.length + value.type.length