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

59 lines
1.9 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
describe('expire-time-infinity', () => {
if (process.env.__NEXT_CACHE_COMPONENTS === 'true') {
it.skip('the route segment config is not compatible with cacheComponents', () => {})
return
}
const { next, skipped } = nextTestSetup({
files: __dirname,
skipStart: true,
skipDeployment: true,
})
if (skipped) return
it('serves a well-formed cache-control header for an Infinity expireTime', async () => {
await next.start()
try {
const res = await next.fetch('/')
const cacheControl = res.headers.get('cache-control')
// The stale-while-revalidate window is derived from the expireTime. An
// Infinity expireTime must resolve to a finite window instead of the
// invalid `stale-while-revalidate=Infinity`.
expect(cacheControl).toMatch(/s-maxage=60, stale-while-revalidate=\d+$/)
} finally {
await next.stop()
}
})
it('fails the build for a non-finite expireTime other than Infinity', async () => {
await next.patchFile('next.config.js', (config) =>
config.replace('expireTime: Infinity', 'expireTime: -Infinity')
)
const { exitCode, cliOutput } = await next.build()
expect(exitCode).not.toBe(0)
expect(cliOutput).toContain(
'Invalid "expireTime" provided, expected a finite number of seconds or Infinity, received -Infinity'
)
})
it('fails the build for a non-finite staleTime other than Infinity', async () => {
await next.patchFile('next.config.js', (config) =>
config
.replace('expireTime: -Infinity', 'expireTime: Infinity')
.replace('dynamic: Infinity', 'dynamic: NaN')
)
const { exitCode, cliOutput } = await next.build()
expect(exitCode).not.toBe(0)
expect(cliOutput).toContain(
'Invalid "experimental.staleTimes.dynamic" provided, expected a finite number of seconds or Infinity, received NaN'
)
})
})