Files
JamesandGitHub df4c2aa8ec fix: revalidation with file-system-cache (#58508)
### What?

When using the file system cache with `isrMemoryCacheSize: 0`, time-based revalidation is not working, and the file is constantly updated. I have also added some debug logging to mirror that in the `fetch-cache` handler

Detailed explanation in #58507

### Why?

The cached object's tags are incorrectly accessed, causing the cache to be rewritten every hit. This is catastrophic for a caching system that relies on file modification timestamps. The tags are one level up in the object from where [they are currently being accessed](https://github.com/vercel/next.js/blob/9ab8828f72e96f5de86a7c50b67a1c5abe6e146c/packages/next/src/server/lib/incremental-cache/file-system-cache.ts#L178).

Below shows a cached fetch representation on disk. When written, the tags reside at `obj.tags` instead of `obj.data.tags`

```json
{
  "kind": "FETCH",
  "data": {
    "headers": {
      "connection": "keep-alive",
      "content-encoding": "br",
      "content-type": "application/json; charset=utf-8",
      "date": "Wed, 15 Nov 2023 21:17:42 GMT",
      "server": "nginx/1.18.0 (Ubuntu)",
      "transfer-encoding": "chunked",
      "vary": "Accept-Encoding"
    },
    "body": "[SNIP]",
    "status": 200,
    "url": "https://timeapi.io/api/Time/current/zone?timeZone=UTC"
    # this is where the current code is trying to pull the tags
    # "tags": [ "time-with-fetch" ]
  },
  "revalidate": 20,
  # tags actually live here
  "tags": [
    "time-with-fetch"
  ]
}
```

Fixes #58507
2023-11-16 23:34:50 +00:00

130 lines
2.9 KiB
TypeScript

import { promises as fs } from 'node:fs'
import { fileURLToPath } from 'node:url'
import FileSystemCache from 'next/dist/server/lib/incremental-cache/file-system-cache'
import { nodeFs } from 'next/dist/server/lib/node-fs-methods'
const cacheDir = fileURLToPath(new URL('./cache', import.meta.url))
describe('FileSystemCache', () => {
it('set image route', async () => {
const fsCache = new FileSystemCache({
_appDir: true,
_pagesDir: true,
_requestHeaders: {},
flushToDisk: true,
fs: nodeFs,
serverDistDir: cacheDir,
revalidatedTags: [],
experimental: {
ppr: false,
},
})
const binary = await fs.readFile(
fileURLToPath(new URL('./images/icon.png', import.meta.url))
)
await fsCache.set(
'icon.png',
{
body: binary,
headers: {
'Content-Type': 'image/png',
},
status: 200,
kind: 'ROUTE',
},
{}
)
expect((await fsCache.get('icon.png')).value).toEqual({
body: binary,
headers: {
'Content-Type': 'image/png',
},
status: 200,
kind: 'ROUTE',
})
})
})
describe('FileSystemCache (isrMemory 0)', () => {
const fsCache = new FileSystemCache({
_appDir: true,
_pagesDir: true,
_requestHeaders: {},
flushToDisk: true,
fs: nodeFs,
serverDistDir: cacheDir,
revalidatedTags: [],
experimental: {
ppr: false,
},
maxMemoryCacheSize: 0, // disable memory cache
})
it('should cache fetch', async () => {
await fsCache.set(
'fetch-cache',
{
kind: 'FETCH',
data: {
headers: {},
body: 'MTcwMDA1NjM4MQ==',
status: 200,
url: 'http://my-api.local',
},
revalidate: 30,
},
{
fetchCache: true,
revalidate: 30,
fetchUrl: 'http://my-api.local',
fetchIdx: 5,
tags: ['server-time'],
}
)
const res = await fsCache.get('fetch-cache', {
tags: ['server-time'],
kindHint: 'fetch',
})
expect(res.value).toEqual({
kind: 'FETCH',
data: {
headers: {},
body: 'MTcwMDA1NjM4MQ==',
status: 200,
url: 'http://my-api.local',
},
revalidate: 30,
tags: ['server-time'],
})
})
it('should cache unstable_cache', async () => {
await fsCache.set(
'unstable-cache',
{
kind: 'FETCH',
data: { headers: {}, body: '1700056381', status: 200, url: '' },
revalidate: 30,
},
{ revalidate: 30, fetchCache: true, tags: ['server-time2'] }
)
const res = await fsCache.get('unstable-cache', {
tags: ['server-time'],
kindHint: 'fetch',
})
expect(res.value).toEqual({
kind: 'FETCH',
data: { headers: {}, body: '1700056381', status: 200, url: '' },
revalidate: 30,
tags: ['server-time2'],
})
})
})