vercel/next.js · #98993

Split revalidation errors by execution context

gnoff · merged Sep 21, 20268 files · 90 + / 12
packages/next/src/server/app-render/work-unit-async-storage.external.ts3 + / 1
@@ -436,10 +436,12 @@ export interface UnstableCacheStore extends CommonCacheStore {  */ export type CacheStore = UseCacheStore | UnstableCacheStore +export type BuildTimeGeneratorName = 'generateStaticParams'+ export interface BuildTimeGeneratorStore extends CommonWorkUnitStore {   readonly type: 'build-time-generator'   /** The public export name to use in diagnostics, regardless of bundling. */-  readonly functionName: string+  readonly functionName: BuildTimeGeneratorName   readonly rootParams: Params } 
packages/next/src/server/use-cache/use-cache-messages.ts22 + / 2
@@ -4,6 +4,8 @@  * immediate fix, then link to the relevant docs.  */ +import type { BuildTimeGeneratorName } from '../app-render/work-unit-async-storage.external'+ const NEXT_REQUEST_IN_USE_CACHE =   'https://nextjs.org/docs/messages/next-request-in-use-cache' @@ -122,12 +124,30 @@ export function createDraftModeMutationInUnstableCacheError( }  export function createRevalidateDuringRenderError(+  route: string,+  expression: string+): Error {+  return new Error(+    `Route "${route}": \`${expression}\` can't be called during render. Call it from a Server Action or Route Handler instead.\nLearn more: ${REVALIDATE_IN_USE_CACHE}`+  )+}++export function createRevalidateInCachedFunctionError(+  route: string,+  expression: string+): Error {+  return new Error(+    `Route "${route}": \`${expression}\` can't be called inside a cached function. Call it from a Server Action or Route Handler instead.\nLearn more: ${REVALIDATE_IN_USE_CACHE}`+  )+}++export function createRevalidateInBuildTimeGeneratorError(   route: string,   expression: string,-  generatorName?: string+  generatorName: BuildTimeGeneratorName ): Error {   return new Error(-    `Route "${route}": \`${expression}\` can't be called during render, inside a cached function, or inside \`${generatorName ?? 'generateStaticParams'}\`. Call it from a Server Action or Route Handler instead.\nLearn more: ${REVALIDATE_IN_USE_CACHE}`+    `Route "${route}": \`${expression}\` can't be called inside \`${generatorName}\`. Call it from a Server Action or Route Handler instead.\nLearn more: ${REVALIDATE_IN_USE_CACHE}`   ) } 
packages/next/src/server/web/spec-extension/revalidate.ts8 + / 7
@@ -14,7 +14,11 @@ import { } from '../../../shared/lib/action-revalidation-kind' import { removeTrailingSlash } from '../../../shared/lib/router/utils/remove-trailing-slash' import { encodeHeaderSafe } from '../../lib/encode-header-safe'-import { createRevalidateDuringRenderError } from '../../use-cache/use-cache-messages'+import {+  createRevalidateDuringRenderError,+  createRevalidateInBuildTimeGeneratorError,+  createRevalidateInCachedFunctionError,+} from '../../use-cache/use-cache-messages' import { validateAndNormalizeCacheLifeProfile } from '../../use-cache/cache-life-profile'  type CacheLifeConfig = {@@ -145,17 +149,18 @@ function revalidate(    const workUnitStore = workUnitAsyncStorage.getStore()   if (workUnitStore) {-    // Keep the generator's name before applying the general render-phase check.+    // Prefer the more specific contexts over the general render-phase error.     switch (workUnitStore.type) {       case 'build-time-generator':-        throw createRevalidateDuringRenderError(+        throw createRevalidateInBuildTimeGeneratorError(           store.route,           expression,           workUnitStore.functionName         )       case 'cache':       case 'private-cache':       case 'unstable-cache':+        throw createRevalidateInCachedFunctionError(store.route, expression)       case 'prerender':       case 'prerender-runtime':       case 'prerender-client':@@ -172,10 +177,6 @@ function revalidate(     }      switch (workUnitStore.type) {-      case 'cache':-      case 'private-cache':-      case 'unstable-cache':-        throw createRevalidateDuringRenderError(store.route, expression)       case 'prerender':       case 'prerender-runtime':         // cacheComponents Prerender
test/e2e/app-dir/revalidatetag-rsc/app/page.tsx3 + / 0
@@ -19,6 +19,9 @@ export default async function Page() {       <Link href="/revalidate_via_page?tag=data" id="revalidate-via-page">         Revalidate via page       </Link>+      <Link href="/revalidate_via_cache" id="revalidate-via-cache">+        Revalidate via cache+      </Link>     </div>   ) }
test/e2e/app-dir/revalidatetag-rsc/app/revalidate_via_cache/page.tsxadded13 + / 0
@@ -0,0 +1,13 @@+import { revalidateTag, unstable_cache } from 'next/cache'++export const dynamic = 'force-dynamic'++const revalidateInCache = unstable_cache(async () => {+  revalidateTag('data', 'max')+})++export default async function Page() {+  await revalidateInCache()++  return <p>revalidated</p>+}
test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts24 + / 2
@@ -30,7 +30,7 @@ describe('revalidateTag-rsc', () => {       if (isNextDev) {         await waitForRedbox(browser)         await expect(getRedboxHeader(browser)).resolves.toContain(-          'Route "/revalidate_via_page": `revalidateTag("data")` can\'t be called during render, inside a cached function, or inside `generateStaticParams`.'+          'Route "/revalidate_via_page": `revalidateTag("data")` can\'t be called during render.'         )       } else {         await retry(async () => {@@ -41,7 +41,29 @@ describe('revalidateTag-rsc', () => {       }        expect(next.cliOutput).toContain(-        'Route "/revalidate_via_page": `revalidateTag("data")` can\'t be called during render, inside a cached function, or inside `generateStaticParams`. Call it from a Server Action or Route Handler instead.\nLearn more: https://nextjs.org/docs/messages/revalidate-in-use-cache'+        'Route "/revalidate_via_page": `revalidateTag("data")` can\'t be called during render. Call it from a Server Action or Route Handler instead.\nLearn more: https://nextjs.org/docs/messages/revalidate-in-use-cache'+      )+    })++    it('should error if revalidateTag is called inside a cached function', async () => {+      const browser = await next.browser('/')+      await browser.elementByCss('#revalidate-via-cache').click()++      if (isNextDev) {+        await waitForRedbox(browser)+        await expect(getRedboxHeader(browser)).resolves.toContain(+          'Route "/revalidate_via_cache": `revalidateTag("data")` can\'t be called inside a cached function.'+        )+      } else {+        await retry(async () => {+          expect(+            await browser.eval('document.documentElement.innerHTML')+          ).toContain('This page couldn\u2019t load')+        })+      }++      expect(next.cliOutput).toContain(+        'Route "/revalidate_via_cache": `revalidateTag("data")` can\'t be called inside a cached function. Call it from a Server Action or Route Handler instead.\nLearn more: https://nextjs.org/docs/messages/revalidate-in-use-cache'       )     })   }
test/production/app-dir/generate-static-params-errors/app/[lang]/revalidate-tag/[slug]/page.tsxadded10 + / 0
@@ -0,0 +1,10 @@+import { revalidateTag } from 'next/cache'++export default function Page() {+  return <p>revalidate-tag</p>+}++export async function generateStaticParams() {+  revalidateTag('data', 'max')+  return [{ slug: 'test' }]+}
test/production/app-dir/generate-static-params-errors/generate-static-params-errors.test.ts7 + / 0
@@ -47,6 +47,13 @@ describe('generate-static-params-errors', () => {     )   }) +  it('should error when revalidateTag() is called inside generateStaticParams', async () => {+    await buildRoute('app/[lang]/revalidate-tag/[slug]/page.tsx')+    expect(getCliOutput()).toContain(+      'Error: Route "/[lang]/revalidate-tag/[slug]": `revalidateTag("data")` can\'t be called inside `generateStaticParams`. Call it from a Server Action or Route Handler instead.\nLearn more: https://nextjs.org/docs/messages/revalidate-in-use-cache'+    )+  })+   it('should allow root params access inside generateStaticParams', async () => {     await buildRoute('app/[lang]/root-params/[slug]/page.tsx')     expect(getCliOutput()).not.toContain('Error')