Skip to content

Commit fd552c5

Browse files
authored
prevent revalidateTag/Path during render (#71093)
Makes `revalidatePath` and `revalidateTag` throw if called during render. Revalidating is a side-effecting operation so it should not be allowed there. x-ref: #70642 (comment)
1 parent 1b21c3a commit fd552c5

File tree

3 files changed

+57
-2
lines changed

3 files changed

+57
-2
lines changed

packages/next/src/server/web/spec-extension/revalidate.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ function revalidate(tag: string, expression: string) {
6060
`Route ${store.route} used "${expression}" inside a function cached with "unstable_cache(...)" which is unsupported. To ensure revalidation is performed consistently it must always happen outside of renders and cached functions. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
6161
)
6262
}
63+
if (workUnitStore.phase === 'render') {
64+
throw new Error(
65+
`Route ${store.route} used "${expression}" during render which is unsupported. To ensure revalidation is performed consistently it must always happen outside of renders and cached functions. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
66+
)
67+
}
6368
}
6469

6570
// a route that makes use of revalidation APIs should be considered dynamic
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use server'
2+
3+
import Link from 'next/link'
4+
import { revalidateTag } from 'next/cache'
5+
6+
const RevalidateViaPage = async ({
7+
searchParams,
8+
}: {
9+
searchParams: Promise<{ tag: string }>
10+
}) => {
11+
const { tag } = await searchParams
12+
revalidateTag(tag)
13+
14+
return (
15+
<div className="flex flex-col items-center justify-center h-screen">
16+
<pre>Tag [{tag}] has been revalidated</pre>
17+
<Link href="/" id="home">
18+
To Home
19+
</Link>
20+
</div>
21+
)
22+
}
23+
24+
export default RevalidateViaPage

test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { nextTestSetup } from 'e2e-utils'
2-
import { retry } from 'next-test-utils'
2+
import { getRedboxHeader, retry } from 'next-test-utils'
33

44
describe('revalidateTag-rsc', () => {
5-
const { next } = nextTestSetup({
5+
const { next, isNextDev, isNextDeploy } = nextTestSetup({
66
files: __dirname,
77
})
88

@@ -20,4 +20,30 @@ describe('revalidateTag-rsc', () => {
2020
expect(randomNumber3).not.toEqual(randomNumber)
2121
})
2222
})
23+
24+
if (!isNextDeploy) {
25+
// skipped in deploy because it uses `next.cliOutput`
26+
it('should error if revalidateTag is called during render', async () => {
27+
const browser = await next.browser('/')
28+
await browser.elementByCss('#revalidate-via-page').click()
29+
30+
if (isNextDev) {
31+
await retry(async () => {
32+
expect(await getRedboxHeader(browser)).toContain(
33+
'Route /revalidate_via_page used "revalidateTag data"'
34+
)
35+
})
36+
} else {
37+
await retry(async () => {
38+
expect(
39+
await browser.eval('document.documentElement.innerHTML')
40+
).toContain('Application error: a server-side exception has occurred')
41+
})
42+
}
43+
44+
expect(next.cliOutput).toContain(
45+
'Route /revalidate_via_page used "revalidateTag data"'
46+
)
47+
})
48+
}
2349
})

0 commit comments

Comments
 (0)