Files
next.js/test/e2e/app-dir/use-cache-hanging/use-cache-hanging.test.ts
Hendrik LiebauandGitHub 8de4b60049 Make 'use cache' fill timeout configurable (#93070)
Applications can now override the `'use cache'` fill timeout through a
new `experimental.useCacheTimeout` config (in seconds, matching the
`staticPageGenerationTimeout` / `cacheLife` configs), which lets them
raise the timeout for slower dev-time backends or lower it for faster
feedback while iterating. Without a value set, the timeout defaults to
90% of `staticPageGenerationTimeout`, landing at 54s against the 60s
default build timeout — slightly higher than the previous hard-coded
50s.

During static and runtime prerendering, the effective timeout is clamped
to `0.9 × staticPageGenerationTimeout` so the cache-fill error surfaces
before the build worker kills the page. In dev mode, the configured
value is used as-is since there's no equivalent build-worker kill. The
defaulting and clamp are centralized in a single
`getUseCacheFillTimeoutMs` helper in `use-cache-wrapper.ts`, replacing
the previous module-level `USE_CACHE_FILL_TIMEOUT_MS` constant.

The new value is threaded through `RenderOpts` and `WorkStore`.
Proxy/middleware and the edge route-module wrapper construct a WorkStore
but never fill `'use cache'` entries, so they pass `0` sentinels with a
comment — if anything ever reads these values, the cache will time out
immediately and surface the bug loudly. We'll revisit this if we ever
add `'use cache'` support to Proxy.

A new `use-cache-configured-timeout` fixture exercises the clamp by
setting `staticPageGenerationTimeout: 10` and flipping `useCacheTimeout`
between `15` (in dev, via `process.env.__NEXT_DEV_SERVER`) and `60` (in
build). Two pages, one sleeping 11s (between the 9s clamp and the 15s
dev timeout) and one sleeping 17s (above the dev timeout), let us assert
that the dev value is applied without clamping and that the build clamps
both pages. The existing `use-cache-hanging` fixture now sets
`useCacheTimeout: 10` so tests don't race the browser's `page.goto`
ceiling, and the explicit 180s test timeouts there have been dropped in
favor of the Jest default.
2026-04-21 22:45:36 +02:00

112 lines
4.0 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
import stripAnsi from 'strip-ansi'
const expectedTimeoutErrorMessage =
'Filling a cache during prerender timed out, likely because request-specific arguments such as params, searchParams, cookies() or dynamic data were used inside "use cache".'
describe('use-cache-hanging', () => {
const { next, isNextDev, skipped, isTurbopack } = nextTestSetup({
files: __dirname,
skipDeployment: true,
skipStart: process.env.NEXT_TEST_MODE !== 'dev',
})
if (skipped) {
return
}
if (isNextDev) {
describe('when a "use cache" fill hangs in the static stage', () => {
it('should show an error toast after a timeout', async () => {
const outputIndex = next.cliOutput.length
const browser = await next.browser('/static')
await expect(browser).toDisplayRedbox(`
{
"code": "E394",
"description": "Filling a cache during prerender timed out, likely because request-specific arguments such as params, searchParams, cookies() or dynamic data were used inside "use cache".",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/static/page.tsx (6:1) @ getCachedData
> 6 | async function getCachedData(): Promise<string> {
| ^",
"stack": [
"getCachedData app/static/page.tsx (6:1)",
"Cached app/static/page.tsx (18:24)",
"Page app/static/page.tsx (32:10)",
],
}
`)
const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))
expect(cliOutput).toContain(`Error: ${expectedTimeoutErrorMessage}
at getCachedData (app/static/page.tsx:6:1)`)
})
})
describe('when a "use cache" fill hangs in the runtime stage', () => {
it('should show an error toast after a timeout', async () => {
const outputIndex = next.cliOutput.length
const browser = await next.browser('/runtime')
await expect(browser).toDisplayRedbox(`
{
"code": "E394",
"description": "Filling a cache during prerender timed out, likely because request-specific arguments such as params, searchParams, cookies() or dynamic data were used inside "use cache".",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/runtime/page.tsx (8:1) @ getCachedData
> 8 | async function getCachedData(): Promise<string> {
| ^",
"stack": [
"getCachedData app/runtime/page.tsx (8:1)",
"Cached app/runtime/page.tsx (20:24)",
"Page app/runtime/page.tsx (42:7)",
],
}
`)
const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))
expect(cliOutput).toContain(`Error: ${expectedTimeoutErrorMessage}
at getCachedData (app/runtime/page.tsx:8:1)`)
})
})
describe('when a "use cache" performs long-running I/O in the dynamic stage', () => {
it('should not time out', async () => {
const outputIndex = next.cliOutput.length
const browser = await next.browser('/dynamic')
await expect(browser.elementByCss('#cached').text()).resolves.toBe(
'cached'
)
const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))
expect(cliOutput).not.toContain(expectedTimeoutErrorMessage)
})
})
} else {
describe('when a "use cache" fill hangs during prerendering', () => {
it('should fail the build with a timeout error', async () => {
try {
await next.start()
} catch {
// expected
}
if (isTurbopack) {
expect(next.cliOutput)
.toContain(`Error: ${expectedTimeoutErrorMessage}
at <unknown> (app/static/page.tsx:6:1)`)
} else {
// Webpack production builds don't have source maps by default.
expect(next.cliOutput).toContain(expectedTimeoutErrorMessage)
}
})
})
}
})