From fd552c5a8880868c788898dc555800fffc8061a5 Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Fri, 11 Oct 2024 16:56:06 +0200 Subject: [PATCH] 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: https://github.com/vercel/next.js/pull/70642#issuecomment-2387219600 --- .../server/web/spec-extension/revalidate.ts | 5 ++++ .../app/revalidate_via_page/page.tsx | 24 +++++++++++++++ .../revalidatetag-rsc.test.ts | 30 +++++++++++++++++-- 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_page/page.tsx 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"' + ) + }) + } })