diff --git a/packages/next/src/server/web/spec-extension/revalidate.ts b/packages/next/src/server/web/spec-extension/revalidate.ts index d3802b2420b25..72898597a16c2 100644 --- a/packages/next/src/server/web/spec-extension/revalidate.ts +++ b/packages/next/src/server/web/spec-extension/revalidate.ts @@ -60,6 +60,11 @@ function revalidate(tag: string, expression: string) { `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` ) } + if (workUnitStore.phase === 'render') { + throw new Error( + `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` + ) + } } // a route that makes use of revalidation APIs should be considered dynamic diff --git a/test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_page/page.tsx b/test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_page/page.tsx new file mode 100644 index 0000000000000..319a31582f655 --- /dev/null +++ b/test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_page/page.tsx @@ -0,0 +1,24 @@ +'use server' + +import Link from 'next/link' +import { revalidateTag } from 'next/cache' + +const RevalidateViaPage = async ({ + searchParams, +}: { + searchParams: Promise<{ tag: string }> +}) => { + const { tag } = await searchParams + revalidateTag(tag) + + return ( +
+
Tag [{tag}] has been revalidated
+ + To Home + +
+ ) +} + +export default RevalidateViaPage diff --git a/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts b/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts index 3eec1764f479b..045cb3e8f45c6 100644 --- a/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts +++ b/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts @@ -1,8 +1,8 @@ import { nextTestSetup } from 'e2e-utils' -import { retry } from 'next-test-utils' +import { getRedboxHeader, retry } from 'next-test-utils' describe('revalidateTag-rsc', () => { - const { next } = nextTestSetup({ + const { next, isNextDev, isNextDeploy } = nextTestSetup({ files: __dirname, }) @@ -20,4 +20,30 @@ describe('revalidateTag-rsc', () => { expect(randomNumber3).not.toEqual(randomNumber) }) }) + + if (!isNextDeploy) { + // skipped in deploy because it uses `next.cliOutput` + it('should error if revalidateTag is called during render', async () => { + const browser = await next.browser('/') + await browser.elementByCss('#revalidate-via-page').click() + + if (isNextDev) { + await retry(async () => { + expect(await getRedboxHeader(browser)).toContain( + 'Route /revalidate_via_page used "revalidateTag data"' + ) + }) + } else { + await retry(async () => { + expect( + await browser.eval('document.documentElement.innerHTML') + ).toContain('Application error: a server-side exception has occurred') + }) + } + + expect(next.cliOutput).toContain( + 'Route /revalidate_via_page used "revalidateTag data"' + ) + }) + } })