next.js: [NEXT-1186] revalidatePath not working for dynamic routes while res.revalidate works fine

Verify canary release

  • I verified that the issue exists in the latest Next.js canary release

Provide environment information

    Operating System:
      Platform: darwin
      Arch: x64
      Version: Darwin Kernel Version 22.5.0: Mon Apr 24 20:51:50 PDT 2023; root:xnu-8796.121.2~5/RELEASE_X86_64
    Binaries:
      Node: 16.17.1
      npm: 8.19.2
      Yarn: N/A
      pnpm: N/A
    Relevant packages:
      next: 13.4.7-canary.1
      eslint-config-next: N/A
      react: 18.2.0
      react-dom: 18.2.0
      typescript: 5.1.3

Which area(s) of Next.js are affected? (leave empty if unsure)

App directory (appDir: true)

Link to the code that reproduces this issue

https://github.com/roxizhauk/revalidate-path CodeSandBox

To Reproduce

Visit /blog/[anything] (e.g. /blog/test ) to generate a dynamic page and check the time shown

To try “unsuccessful” app directory’s revalidatePath:

  1. hit /api/revalidate-path?path=/blog/[anything] and you’ll see “revalidated”
  2. refresh /blog/[anything] and you’ll see the time not changed

To try “successful” pages directory’s res.revalidate:

  1. hit /api/revalidate?path=/blog/[anything] and you’ll see “revalidated”
  2. refresh /blog/[anything] and you’ll see the time changed

Describe the Bug

The app directory’s revalidatePath works fine for “/” or “/blog” but dynamic routes like “/blog/1” or “/blog/test” while pages directory’s res.revalidate works fine for all

Expected Behavior

revalidatePath works the same as res.revalidate for dynamic routes

Which browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

No response

From SyncLinear.com | NEXT-1186

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 29
  • Comments: 76 (12 by maintainers)

Commits related to this issue

Most upvoted comments

Hi, the revalidatePath() behavior has now been fixed to correctly revalidate specific URLs instead of just page paths in v13.4.20-canary.27 of Next.js, please update and give it a try!

An example of the issue that was fixed here, when you only want to revalidate /blog/post-1 you can now pass that to revalidatePath instead of having to invalidate all paths for /blog/[slug]/page.

Okay - Seems like we can use unstable_cache to achieve the same functionality as fetch tag invalidation, but for any type of data fetching.

import { unstable_cache } from 'next/cache'

export default async function post({
  postId
}: {
  postId:string
}) {
const posts = await unstable_cache(
    async () => {
      return prisma.post.findUnique({
        where: {
         postId: postId,
        }
      })
    },
    [postId],
    { tags: [postId] }
  )()

Then you can invalidate the post by invalidating the postId as a tag like so: revalidateTag(postId)

At the moment res.revalidate() can achieve this for ISR paths, we are investigating this behavior for revalidatePath() though.

I can confirm that I’ve lost brain cells on this as well. Recap of the above posts in this thread is that right now is that there is no way to bust cache for a specific page.

  • revalidatePath only works with “router paths” and not URIs, like the old one worked. Meaning /test and /test/[slug] work but /test/1 doesn’t. The caveat being, of course, that /test/[slug] is a wildcard revalidate which ain’t very useful.

  • Tagging works as an alternative, but it is only supported by next’s fetch, meaning that we can’t tag stuff freely. Some people have used unstable_cache function to tag async calls. The caveat being that it is meant for cache control over individual async actions, not whole pages, meaning we have to invent tag naming conventions ourselves.

  • There is no clear answer on how generateStaticParams cache is handled. We have confirmed that unstable_cache with tags doesn’t seem to do anything within generateStaticParams, probably due to the way the router works. @jwalcher

If one is brave or desparate and still choses to do the tagging, I would say it is in fact the most correct way of doing this as it is basically what revalidatePath is doing as well. Unfortunately it is not a stabile API, it is entirely undocumented, not pretty to look at, and very implicit in usage, and finally, it doesn’t just invalidate the tag - it caches the data, meaning it performs data transformations, and there are other GithHub issues open about those being inaccurate. 😭

For reference, for future readers, attaching an example of how I do that currently:

export default async function Post({ params }: { params: { slug: string } }) {
  const post = await unstable_cache(
    async () => await Posts.get(params.slug), [], {
        tags: [`Posts.get-${params.slug}`]
    })();
  return <>Hello {post.name}</>;
}


// Elsewhere
revalidateTag(`Posts.get-${params.slug}`);
revalidateTag(`Posts.all`);

So, there effectively is no way to exert control over cache invalidation. Which was supposed to be the strongest point of the App router performance…

Note: we are investigating this behavior more and will add updates here

I stumbled upon Vercel’s take on how to do this with tags 😃 https://github.com/vercel/commerce/blob/main/app/api/revalidate/route.ts

@leerob The issue isn’t that we don’t understand the intent of the approach, instead it is the cardinality of it - it isn’t very sane to revalidate 7000 product pages every time any product changes. We need to be more granular with this, and the old uri-based API allowed that out of the box. Now, we either have to make it ourselves, which is a pita, or use pages router for this singular purpose.

I agree that the tags approach is healthier in the long run, and able to tightly integrate cache busting with next. Its just a pain to write granular tagging mechanisms ourselves, that is all. So I’m kinda hoping to see a package/utility pop up that would offer a better devex when using tagging.

Is there any possibility of adding tag as a const in the page functions. So instead of cache tags only working in fetch requests, it could work for any type of data fetching (E.g. using Prisma)

Example if we have app/[id]/page.tsx

export default async function Post(params:{id:string}){
   const TAG = params.id
   const post = await prisma.post.findUnique({
     where: {
        id: params.id
      }
}

That way, we could invalidate whichever data fetching strategy is being used (be it fetch or directly from database or whichever)

Hi, the revalidatePath() behavior has now been fixed to correctly revalidate specific URLs instead of just page paths in v13.4.20-canary.27 of Next.js, please update and give it a try!

An example of the issue that was fixed here, when you only want to revalidate /blog/post-1 you can now pass that to revalidatePath instead of having to invalidate all paths for /blog/[slug]/page.

The problem is still here. But in a smaller effect.

I’ve been testing revalidatePath with different parameters on v13.5.3. For example, revalidatePath("/(main)/cases/[slug]", "page") worked fine.

But when I tried to

const path = `/cases/${slug}`;
revalidatePath(path);

it worked only when I was refreshing and staying on the exact same page. Coming from the other page or switching to other page made the cache stale and I could not revalidate anymore. So I had to rebuild the whole project. The bad thing that it’s hard to figure out visiting which page makes the cache impossible to revalidate.

I can revalidate the page just putting the url in the browser. It works many times in a row. But when I start to go between the pages, at some point revalidation stops working.

I am using res.revalidate approach from pages router which at least works. I could not understand at which point App router revalidatePath stops working. The issue is still there when trying to revalidate a specific path from the dynamic route.

This is actually pretty critical - Essentially the previous res.revalidate supported this functionality.

To allow for dynamic revalidation, I tried to use a /pages/api endpoint to revalidate a dynamic path located in my app directory, however, this also fails because (https://github.com/vercel/next.js/issues/48771) res.revalidate requires getStaticProps, which the app directory does not use anymore.

This means that effectively there’s no way to revalidate dynamic paths in the app directory. You can use the tag revalidation, but that is only compatible with fetch. Use cases where data is fetched directly from a database can therefore not make use of on-demand revalidation for dynamic paths.

This means that we have two options: a) Move all routes to the pages directory or b) Use a sub-optimal revalidation strategy where you (e.g.) invalidate ALL blogpost [ids] versus just id 1 when id 1 changes.

Both seem sub-optimal

We are experiencing the same issue. Using the examples above:

App Router

revalidatePath("/blog/[id]") -> works locally (pnpm build && pnpm start), but does not work on Vercel preview or production deployments.

revalidatePath("/blog/first") -> does not work

Pages Router

await res.revalidate("/blog/first"); -> works locally and on Vercel

@roxizhauk from your reproduction steps it seems you are not calling revalidatePath() with the correct pathname as it needs to math the name of the page on your filesystem e.g. app/blog/[id]/page it should be revalidatePath(/blog/[id]) not revalidatePath(/blog/first)

Related response here #49778 (comment)

There’s no way to trigger a revalidation of a specific dynamic route?

same here, revalidatePath() for dynamic routes not working

using “next”: “13.5.4”

This issue still persists

I recently implemented tagging all fetches and calling revalidateTag via webhooks from my CMS, to get a fine-grained revalidation at the level of individual pages. However, leaving aside the extra work, I would still argue that at the moment it does not work according to specs, as documented in https://nextjs.org/docs/app/building-your-application/caching#data-cache

Namely, in production, after revalidateTag is executed on the server, we need two client requests to see the new content. According to the docs, the cache should be purged immediately, and the new content show on the first client request. (In dev-mode, one request is indeed sufficient.)

If anyone can confirm this diagnosis, we should perhaps file a new bug.

Sorry if this wasn’t clear – Route Handlers are revalidate are stable. I’m referring your comment about unstable_cache().

So I’m kinda hoping to see a package/utility pop up that would offer a better devex when using tagging.

This is what unstable_cache(), when stable, will do!

The lack of documentation of these issue is a bit surprising.

@jwalcher to be clear, you are referring to an unstable API that will change and should not yet be adopted. Unstable features are not published to the documentation for this reason.

@edjust please open a separate issue with a reproduction 🙏

It is kind of weird that Next, a framework known for championing caching stuff into the mainstream, released a version that just glosses over the whole caching thing, in a release that was supposed to be all about caching

I don’t know if I’d say we “glossed over it” – it’s documented! But we are working to make the documentation better and add diagrams to help explains concepts. That should be landing shortly https://github.com/vercel/next.js/pull/52514.

Experiencing the same issue as well. revalidatePath doesn’t seem to be working correctly. The only page/route I’m able to revalidate seems to be the root /. Revalidate calls to dynamic routes such as posts/post-1 don’t seem to be working.

Using version: 13.5.4

@leerob oh, I wasn’t aware that it was unstable, that changes things a lot and I’m sorry for saying “glossed over”. I was under the impression that both Route Handlers and revalidate are stable. Probably as there is no warning on their docs saying they are unstable 😕

In any case, I don’t think there is a misunderstanding of diagrams here, its purely that a major piece of old functionality is missing entirely.

@jwalcher Absolutely agree. It is kind of weird that Next, a framework known for championing caching stuff into the mainstream, released a version that just glosses over the whole caching thing, in a release that was supposed to be all about caching 😄

How is it working for you right now? Via pages router? Can you tell us a bit more please, I would find it very useful, as am sure will others.

I guess you are using the pages router??

export default async function handler(
	_req: NextApiRequest,
	res: NextApiResponse,
) {
	await res.revalidate(`/test`);
	await res.revalidate(`/test/slug`);
	return res.json({ revalidated: true });
}

We have a website coupled to a headless cms and almost all of our pages are in a catchall route. app/[[…slug]]. Some are static prebuild with generateStaticParams. With revalidatePath it looks like I can only revalidate /[[…slug]] , which would basically invalidate almost every page in the website. This is working, however it seems to me if we could invalidate a single page this would be more efficient?

Same here. Any revalidatePath calls to dynamic routes such as revalidatePath("posts/1") don’t seem to be working as expected.

Using version: 13.5.4

@leerob to follow up on @markomitranic – the issue for me is that the development of the (as you say) unstable revalidatePath/revalidateTag API has broken the old res.revalidate() (at least for me). So I’m a bit stuck between a rock and a hard place here. Otherwise happy to wait!

Yes, I have a route handler in the pages directory calling res.revalidate on individual routes which themselves are in the app directory. I even had a workaround to revalidate also in dev mode by simply clearing the cache altogether

pages/api/revalidate.ts

const directory = process.cwd() + "/.next/cache/fetch-cache";

export default async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
    if (process.env.NODE_ENV === "development") {
      console.log("clearing cache");
      fs.readdir(directory, (err, files) => {
        for (const file of files) {
          fs.unlink(path.join(directory, file), (err) => { if (err) throw err; });
        }
      });
      return res.json({ cacheCleared: true });
    } else {
      const pathsToRevalidate = revalPaths(req.body);
      console.log("revalidating", pathsToRevalidate);
      // The argument of res.revalidate should be the path of the actual route not a rewritten or
      // dynamically generated one, e.g. for "/blog/[slug]" this should be "/blog/post-1"
      await Promise.all(pathsToRevalidate.map((path) => res.revalidate(path)));
      return res.json({ revalidated: true });
    }
}

This strategy was working fine until v13.3, at which point some changes introduced in the Next server led to interference with my middleware, see https://github.com/vercel/next.js/issues/50464 Now I could work around the middleware, but my hack to revalidate in dev mode definitely does not work anymore starting in v13.5 or so. This all sounds convoluted, and it is, but my feeling is that the issues (with the old method) would be fairly easy to resolve if somebody cared to look. Alternatively, I’d be happy to wait if there was a clear statement about what the plans are to finally address the issues that @markomitranic you so well summarized.

Sorry if I am misunderstanding, but are you now calling revalidatePath on dynamically generated routes /posts/1, /posts/2, etc. or do we still need to put the path /posts/[id]

Also, a general question about ISR: How do people deal with deleting specific routes when the content is deleted from CMS?

Ask general questions somewhere like the community Discord - not this Issue which is about a specific topic.

@roxizhauk I would say yes - always good to have an updated reproduction with the latest version.

If you could make a demo on CodeSandbox or StackBlitz this can also make it easier for others to set up the environment to see the problem, confirm the behavior and try out fixes

@levipadre The way I handled this was with a tag in the fetch with the slug of the page.

With your fetch tagged, you can pass that as the url parameter to the revalidate API call.


I worked with another dev on the project to discover this, so if you’re interested in why it works this way:

Firstly, it’s worth noting that revalidatePath actually just calls revalidateTag (file here). So rather than differentiating between them, let’s stick with tag for everything.

Next is caching your fetch responses on the filesystem inside .next/cache/fetch-cache. There are a bunch of generated JSON files which are the fetch responses themselves, and then a manifest which maps a tag to a JSON filename

Checking this manifest, you’ll see that it’s not actually caching against the slugs of your pages, but against the filesystem path (the one with dynamic segements in it). As an example, here’s an excerpt from mine:

"/[locale]/[[...slug]]/page": {
      "keys": [
        "97579d80f5605b49cad029b3856c33395a948fe95d061a2209d11bc4eedd98ee",
        "df56bce481a809756e395b558faf3f902327952b220f7c5b39b16c86518bb769"
      ]
    },

Calling revalidate against a specific slug (like /my-page) won’t revalidate anything because that key isn’t in the manifest.

You can see there are various keys under a dynamic segment route, and these relate to how many slugs have been hit for that route. E.g. my first key is for /en/my-page and the second for /en/my-second-page.

I could revalidate /[locale]/[[...slug]]/page, but then every single one of my pages’ fetch calls will be revalidated, which isn’t ideal when you just want to do one page. That’s why adding a custom tag and revalidating against that worked well for us.

Hi, I ended up here while looking for a way to provoke hard navigate:

  • I open /response/1234/sections/1
  • The layout /response/[responseId]/layout.tsx loads the right response and put it into a client context. The layout uses “force-dynamic”.
  • I send a POST request to update this response
  • I use `router.push(“/response/1234/sections/2”) => currently the response won’t be updated, there is no refetch

So I changed the router.push to window.location.href to provoke a refresh but that’s not very good. I might instead create an endpoint to get fresh data from the client but that kinda defeats the purpose of the RSC.

Is this what you tried to achieve @roxizhauk?

All those revalidate method seems to target static/ISR, to invalidate the server cache, but I struggle to find a solution to provoke hard navigates client-side after an update, telling to router.push("/response/1234") that response “1234” has been modified since the page was loaded.

Found the issue in my code @khristovv. My UI was not updating correctly, however if I logged on the server whatever is returned from the serverAction, it was updating correctly. Try to log the returned value from your API and see if it changes correctly, if it does then revalidatePath() is working well, the issue might be somewhere else.

I am using a dynamic route /dashboard/properties/[id]

1 - Getting the propertyDetails in a async server component.

propertyDetails = await fetchProperty({ propertyId: params.id });

2- passing the details from server component to -> client component and calling the updatePropertyDetails with revalidatePath from the client component.

3- The UI now is updating correctly as long as I only use the data coming directly from the propertyDetails from the server component, without changing the data

Scrap my old comment, seems to be working correctly, my mistake was assigning the propertyDetails to a new variable in the client component, try to use the return value directly

For point 2, there are these issues related to TypeError: fetch failed, maybe related:

Hi, the revalidatePath() behavior has now been fixed to correctly revalidate specific URLs instead of just page paths in v13.4.20-canary.27 of Next.js, please update and give it a try!

An example of the issue that was fixed here, when you only want to revalidate /blog/post-1 you can now pass that to revalidatePath instead of having to invalidate all paths for /blog/[slug]/page.

Thank you! I confirm that now both updating the cache of one page and updating the cache of the entire route works, using the second argument “page”. I just suggest adding to the documentation a variation using routing groups. If you need to clear the entire route segment - revalidatePath(“/(main)/post/[slug]”,“page”). This is not very obvious, given that to clear one page you need to set evalidatePath("/post/post1).

I’m really glad this problem is finally fixed. It would also be great if you could help bring the developers’ attention to another cache-related issue that has been active for over 6 months. If the page has dynamic parameters, the 404 error is returned only the first time the page is rendered in production. All the following responses return a 200 response. #43831 #48342 #51021 #45801

What I currently understand (though it’s merely a re-phrase of https://github.com/vercel/next.js/issues/49387#issuecomment-1552394378) is:

  • App router’s revalidatePath works for /blog or ex. /blog/[id] to revalidate all segments under /blog
  • Pages router’s res.revalidate works for /blog/[a-specific-id] to revalidate the individual page

So I guess we just use them separately for now until they deprecate pages/

I’m using 13.4.19 and revalidate path doesn’t work for nested routes however my revalidate route handler return 200. @leerob is this api stable? Thanks

Operating System: Platform: darwin Arch: x64 Version: Darwin Kernel Version 21.6.0: Thu Sep 29 20:12:57 PDT 2022; root:xnu-8020.240.7~1/RELEASE_X86_64 Binaries: Node: 18.13.0 npm: 8.19.3 Yarn: 1.22.10 pnpm: 8.3.1 Relevant packages: next: 13.3.0 eslint-config-next: 13.3.0 react: 18.2.0 react-dom: 18.2.0

My Next.js project is not functioning properly in the production environment (Firebase Hosting). When I run the build, export, and start commands locally on my machine, everything works fine. However, I’m encountering issues with the revalidate and fallback features not working as expected in the production environment.

I have already updated my Next.js version to 13.3.0 based on this discussion https://github.com/vercel/next.js/discussions/42290#discussioncomment-5502200, but the issue persists.

I expect the revalidate and fallback features to work correctly in the production environment as they do on my local machine.

Actual Behavior: In the production environment, the revalidate and fallback features are not functioning as expected. The pages do not update automatically with the specified revalidate interval, and the fallback behavior is not working with new params that wasn’t specified initially.

My page is /profile/[id].tsx

const Profile: NextPage<ProfileProps> = ({ profile }) => {
  const { isFallback } = useRouter()

  if (isFallback) {
    return <Loading />
  }

  return (
   ...
  )
}

export const getStaticPaths: GetStaticPaths = async () => {
  const urlIds = ['1']
  const paths = urlIds.map((urlId) => ({
    params: { id: urlId }
  }))

  return { paths, fallback: true }
}

export const getStaticProps: GetStaticProps<ProfileProps> = async ({ params }) => {
  const urlId = params?.id

  const response = await api.get(`profiles/${urlId}`)
  const profile: Profile | null = response.data?.profile || null

  return { props: { profile }, revalidate: 30 }
}

export default Profile

Sorry if I am misunderstanding, but are you now calling revalidatePath on dynamically generated routes /posts/1, /posts/2, etc. or do we still need to put the path /posts/[id]

Also, a general question about ISR: How do people deal with deleting specific routes when the content is deleted from CMS?

@roxizhauk from your reproduction steps it seems you are not calling revalidatePath() with the correct pathname as it needs to math the name of the page on your filesystem e.g. app/blog/[id]/page it should be revalidatePath(/blog/[id]) not revalidatePath(/blog/first)

Related response here https://github.com/vercel/next.js/issues/49778#issuecomment-1547028830