Files
5af030c959 Normalize and validate expire and revalidate values to handle Infinity and surface mistakes early (#95493)
Supersedes https://github.com/vercel/next.js/pull/93475

## Summary

There's a few fixes bundled in here.

We're fixing `Infinity` getting serialized to `null`, which caused cache
handlers and build workers to misinterpret it as "don't cache" instead
of "cache forever". We use the existing infinity sentinel for that.

We're also making the checks a bit more specific from `typeof !==
'number'` to forbidding `NaN` and `-Infinity`. The checks are applied
consistently for profiles, inline cache life, and arguments to `fetch`
and `unstable_cache`.

Now that configs are also validated, we need to make sure that we only
do `revalidate > expire` validation when both values are specified.
There's a regression test for that, which
https://github.com/vercel/next.js/pull/93475 didn't do correctly.

Also, per review below:

- Now actually using returned value for `unstable_cache`
- Added validation + infinity fix for `revalidateTag` profile
- Added validation + infinity fix in `config.experimental.staleTimes`
and `config.expireTime`
- Added deep clone for `defaultConfig.cacheLife`

## Test Plan

New e2e tests that fail before the fixes.

---------

Co-authored-by: Zack Tanner <[email protected]>
2026-07-09 02:00:38 +01:00

96 lines
2.6 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
const validPage = `export default function Page() {
return <p>hello</p>
}`
describe('use-cache-invalid-cache-life', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipStart: true,
skipDeployment: true,
})
if (skipped) return
it('fails the build for a config profile with a non-finite value other than Infinity', async () => {
await next.patchFile(
'next.config.js',
`module.exports = {
cacheComponents: true,
cacheLife: {
invalid: { revalidate: -Infinity },
},
}`
)
await next.patchFile('app/page.tsx', validPage)
const { exitCode, cliOutput } = await next.build()
expect(exitCode).not.toBe(0)
expect(cliOutput).toContain(
'Invalid "cacheLife.invalid.revalidate" provided, expected a finite number of seconds or Infinity, received -Infinity'
)
})
it('fails the build for a config profile with a revalidate longer than its expire', async () => {
await next.patchFile(
'next.config.js',
`module.exports = {
cacheComponents: true,
cacheLife: {
inconsistent: { revalidate: 100, expire: 50 },
},
}`
)
await next.patchFile('app/page.tsx', validPage)
const { exitCode, cliOutput } = await next.build()
expect(exitCode).not.toBe(0)
expect(cliOutput).toContain(
'the expire option must be greater than the revalidate option'
)
})
it('does not fail the build for a partial default profile whose backfilled revalidate is longer than its expire', async () => {
await next.patchFile(
'next.config.js',
`module.exports = {
cacheComponents: true,
cacheLife: {
default: { expire: 0 },
},
}`
)
await next.patchFile('app/page.tsx', validPage)
const { exitCode } = await next.build()
expect(exitCode).toBe(0)
})
it('fails the build for an inline cacheLife() profile with a non-finite value other than Infinity', async () => {
await next.patchFile(
'next.config.js',
`module.exports = {
cacheComponents: true,
}`
)
await next.patchFile(
'app/page.tsx',
`import { cacheLife } from 'next/cache'
export default async function Page() {
'use cache'
cacheLife({ expire: NaN })
return <p>never rendered</p>
}`
)
const { exitCode, cliOutput } = await next.build()
expect(exitCode).not.toBe(0)
expect(cliOutput).toContain(
'Invalid `cacheLife()` option "expire" provided, expected a finite number of seconds or Infinity, received NaN.'
)
})
})