Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEXT-1186] revalidatePath not working for dynamic routes while res.revalidate works fine #49387

Closed
1 task done
roxizhauk opened this issue May 7, 2023 · 76 comments · Fixed by #55083
Closed
1 task done
Labels
area: app App directory (appDir: true) bug Issue was opened via the bug report template. linear: next Confirmed issue that is tracked by the Next.js team. locked

Comments

@roxizhauk
Copy link

roxizhauk commented May 7, 2023

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

@roxizhauk roxizhauk added the bug Issue was opened via the bug report template. label May 7, 2023
@github-actions github-actions bot added the area: app App directory (appDir: true) label May 7, 2023
@roxizhauk
Copy link
Author

roxizhauk commented May 14, 2023

#49778 (comment)

@ijjk
Copy link
Member

ijjk commented May 14, 2023

@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)

@ijjk ijjk closed this as completed May 14, 2023
@ijjk
Copy link
Member

ijjk commented May 15, 2023

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

@ijjk ijjk reopened this May 15, 2023
@danteissaias
Copy link

@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?

@timneutkens timneutkens added the linear: next Confirmed issue that is tracked by the Next.js team. label May 17, 2023
@timneutkens timneutkens changed the title revalidatePath not working for dynamic routes while res.revalidate works fine [NEXT-1186] revalidatePath not working for dynamic routes while res.revalidate works fine May 17, 2023
@ijjk
Copy link
Member

ijjk commented May 18, 2023

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

@eric-burel
Copy link
Contributor

eric-burel commented May 26, 2023

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.

@roxizhauk
Copy link
Author

@eric-burel
I don't think it is. What I'm trying to do here is on-demand ISR for dynamically generated static pages.

I just quickly built a simple mock with your routing and timing function that I used for my repo but it properly changes the time both on layout and pages. It's weird that your layout isn't updated even with export const dynamic = "force-dynamic" I think you should post the issue with a repo.

@kavehsaket
Copy link

kavehsaket commented May 28, 2023

Same issue with revalidate path. The path I'm trying to revalidate is like ==> [ws]/blogs/[id]. Also, if I use GET for revalidate I get 405 and If I use POST, it won't be any error. However, in your docs, you suggest using GET for on-demand re-validation.
https://nextjs.org/docs/app/api-reference/functions/revalidatePath

@tutorialcode
Copy link

@ijjk Hi, is the revalidatePath function designed to be revalidating an individual path or the route?

@matthewwilson
Copy link

matthewwilson commented May 29, 2023

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

@hauptrolle
Copy link

Same problem here. Event though the revalidation is not working for /en/foo/bar/ nor [lang]/[[...slug]]/

@levipadre
Copy link

levipadre commented Jun 8, 2023

How should I revalidation from the url if my entire site's route is dynamic? app/[lang]/[[...slug]]/page
I thought maybe https://domain.com/api/revalidate?path=/foo-bar but it doesn't revalidate.
I have the revalidation code in pages, but I tried in app, but none worked.

I also have a middleware, as I have different domain names, like domain.it and domain.fr etc.

My code:

export async function getPageByUrl(slug: string[]) {
    try {
        const path = (typeof slug === 'undefined' || slug[0] === 'index') ? '' : slug.join('/');
        const res = await fetch(`https://domain.com/api/posts?url=/${path}`)
        return await res.json();
    } catch (error) {
        console.log(error);
        return { notFound: true };
    }
}

export async function generateStaticParams() {
    return [];
}

export default async function Page({
    params
}: {
    params: {
        slug: string[]
    }
}) {
    const slug = params.slug;
    const [PAGE] = await Promise.all([getPageByUrl(slug)]);

    return (
        <div className='container'>
            <div dangerouslySetInnerHTML={{__html:PAGE[0].body}}></div>
        </div>
    )
}

@sdjnes
Copy link

sdjnes commented Jun 8, 2023

@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.

@levipadre
Copy link

Thank you for the explanation @sdjnes. I did check the manifest in fetch-cache and I saw two tags: "tags":["/[lang]/[[...slug]]/page","/[[...slug]]/page"], so I tried domain.com/api/revalidate?tag=/[lang]/[[...slug]]/page which didn't revalidate, but /api/revalidate?tag=/[[...slug]]/page did revalidate.

So yes, I assume it revalidated all pages in this case. I have a few thousand pages generated, so obviously, I don't want to revalidate all of them.

The only thig that I don't understand, that if I add a tag to my fetch, like this:

export async function getPageByUrl(slug: string[]) {
    try {
        const path = (typeof slug === 'undefined' || slug[0] === 'index') ? '' : slug.join('/');
        const res = await fetch(`https://domain.com/api/posts?url=/${path}`, {
            next: {
                tags: ['posts'],
            }
        })
        return await res.json();
    } catch (error) {
        console.log(error);
        return { notFound: true };
    }
}

and I call domain.com/api/revalidate?tag=posts, it's still going to revalidate all the pages, right?

What I would like to do is to revalidate only one page. There is a headless CMS, and all the data comes from a restAPI. When an editor modifies a page (e.g. https://domain.com or https://domain.com/foo or https://domain.com/foo/bar etc.), I would like to update the content on this page.
Same situation when someone creates a new page or deletes one.

Is there any way to do that? Maybe dynamically add tags to all pages based on their path?

@sdjnes
Copy link

sdjnes commented Jun 8, 2023

You can use the slug as the tag. This is how ours looks:

const slugString = `/${slug.join('/')}`;

...
    next: {
      tags: [
        slugString === '/' ? '/homepage' : slugString,
        ]
    }
...

The edge-case for the homepage is because calling revalidateTag('/') will revalidate everything in the cache, which is obviously not what we want to do.

Then, in our CMS, we have an onUpdate hook that calls the frontend with the slug of the page (with the logic to handle the homepage edge case) -- something like /api/revalidate?tag=/my-cms-page

@levipadre
Copy link

levipadre commented Jun 8, 2023

Wow, thank you so much @sdjnes . You are a lifesaver! It works like a charm.

You have already helped a lot, but if you don't mind, I would like to ask one more thing. Also cache related, and it seems you know your way around this topic.

My menu is acting weird cache-wise. The menu also generated by json and however I added a tag to it, it just doesn't want to cache it. Whenever I change the menu data, it refreshes immediately.

The menu is in a header, which is a client component. My code look like this:

export async function getMenu() {
    try {
        const res = await fetch(`https://domain.com/api/menu`, {
            next: {
                tags: ['menu'],
            }
        })
        return await res.json();
    } catch (error) {
        console.log(error);
        return { notFound: true };
    }
}

the header

"use client";

import Container from 'react-bootstrap/Container';
import Nav from 'react-bootstrap/Nav';
import Navbar from 'react-bootstrap/Navbar';
import Link from 'next/link';

import React, { useEffect, useState } from 'react';
import { getMenu } from '@/lib/APIs/menu';
import { use } from 'react';

const dataPromise = getMenu();

const Header = () => {
    const MENU = use(dataPromise);

    return (
        // use ClientOnly to prevent hydration mismatch
        <ClientOnly>
            <header>
                <Navbar>
                    <Container>
                        <Link href='/'>Logo</Link>
                        <Navbar.Collapse>
                            <Nav>
                                {MENU.map((item: any) => {
                                    return (
                                        <Nav.Item key={item.id}>
                                            <Link href={item.url} prefetch={false}>{item.title}</Link>
                                        </Nav.Item>
                                    );
                                })}
                            </Nav>
                        </Navbar.Collapse>
                    </Container>
                </Navbar>
            </header>
        </ClientOnly>
    );
}

export default Header;

export default function ClientOnly({ children }: { children: React.ReactNode }) {
    const [canRender, setCanRender] = useState(false);
    useEffect(() => {
        setCanRender(true);
    }, []);
  
    if (!canRender) return null;
  
    return <>{children}</>;
}

and I just import to the layout.

Can I cache the same way as the posts?

@lukepearce
Copy link

lukepearce commented Jun 8, 2023

@levipadre - the above is related to this issue. If you need help with something else there are loads of other places to do so. This issue's comments should be kept on topic to avoid confusion and therefore extra effort for the maintainers.

@levipadre
Copy link

Yes, it's true, I understand. I took a shot. Thanks anyway

@levipadre
Copy link

Doing some further testing, I noticed an issue. I don't know if it's only my code or generic, but when I revalidate /foo page, it also revalidates /foo/bar page too.
By checking .next/cache/fetch-cache, I saw that every tag does contain my generated tags, but it also contains a tag too (not by me), which is the same in every file.
E.g.:

"tags":["/foo","/[[...slug]]/page"]

and

"tags":["/foo/bar","/[[...slug]]/page"]

...so I assume because of the common /[[...slug]]/page"] tag whenever I call a revalidation it revalidated every pages.

@levipadre
Copy link

Could you share your code @jwalcher? Just to compare

@jwalcher
Copy link

app/[category]/page.js

export default async function CategoryPage({ params }) {
    const endpoint = params.category
    const pageData = await fetch(`${global.fetchURI}${endpoint}`, {
        next: { tags: [params.category] }
    })
return <> {pageData} </> 
}

app/api/revalidate/route.js

const revalPayloadToTags = (payload) => { 
return [payload.category]
}

export async function POST(request) { 
  const payload = await request.json();
  const tags = revalPayloadToTags(payload);
  for (let tag of tags) { revalidateTag(tag); }
   return NextResponse.json({ revalidated: true }, { status: 200 });
}

@kavehsaket
Copy link

kavehsaket commented Aug 21, 2023

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

@roxizhauk
Copy link
Author

What I currently understand (though it's merely a re-phrase of #49387 (comment)) 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/

ijjk added a commit that referenced this issue Sep 12, 2023
This updates docs for the fixes landed in
#53321 related to
`revalidatePath`.

Fixes: #49387

---------

Co-authored-by: Lee Robinson <me@leerob.io>
@ijjk
Copy link
Member

ijjk commented Sep 14, 2023

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.

@DennieMello
Copy link

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

@ijjk
Copy link
Member

ijjk commented Sep 18, 2023

@DennieMello thanks for the flag, opened #55542 and #55543 addressing those.

ijjk added a commit that referenced this issue Sep 18, 2023
This ensures we properly set/restore the status code with ISR paths in
app router so that when we set the 404 status code with `notFound` it is
persisted properly.

Fixes: #43831
Closes: #48342
x-ref:
#49387 (comment)
ijjk added a commit that referenced this issue Sep 18, 2023
Adds explicit examples for route groups as those may not be obvious from
existing examples.

x-ref:
#49387 (comment)
@jwalcher
Copy link

Thanks @ijjk! Now trying to update from 13.3.0 to 13.5.1, two comments:

  1. Tests with revalidatePath on dynamics routes are encouraging. One remaining confusion is the timing of cache invalidation. According to the general docs, one would expect that the cache should be cleared immediately when calling revalidatePath. The API description however says that this will only happen when the path is next visited. This is the behavior I'm seeing, at least in production we have to refresh the routes twice before seeing the new data. That's not quite optimal in my mind.

  2. My production app uses a third-party library (apollo graphql) for data fetching. On demand revalidation might or might not work, but for the time being I am getting a “Failed to set fetch cache TypeError: fetch failed on every fresh build. Is that expected or is the entire caching mechanism not yet working for third party libraries (and we have to wait for the unstable_cache api)? (fwiw, the trouble seems to start at 13.4.13, which is also when res.revalidate works again for me)

I'll keep trying, does either of these sound worthy of a bug report?

@karlhorky
Copy link
Contributor

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

@jwalcher
Copy link

jwalcher commented Sep 20, 2023

Looks related indeed. Funny thing over here is that if I first build in an old version (say 13.4.12), upgrade to 13.3.13 and then build again, there's no error. It only appears after I delete .next/cache and build fresh.

Update: Most closely related issue to second point is #53695, not directly related to revalidation

Update: Indeed, issue 2 can be fixed by other means. I'm still curious about 1 though, when exactly revalidation is supposed to happen.

@DennieMello
Copy link

DennieMello commented Sep 21, 2023

@DennieMello thanks for the flag, opened #55542 and #55543 addressing those.

Thank you for fixing the error so quickly!

When using revalidatePath(/(main), "page") to clear the main page cache, the cache of all pages located in (main)/page/[page]/page.tsx is also cleared along with it. In other folders that are not named "page", the cache is not cleared. So the problem seems to be caused by a comparison with the name "page". I think this is a special case and this problem clearly does not require an urgent fix, I just noticed non-obvious behavior.

@roxizhauk
Copy link
Author

roxizhauk commented Sep 24, 2023

@ijjk Dynamic routes visited through <Link> won't be updated when using revalidatePath whereas res.revalidate works as expected. Seems like the cache in the client-side (Router Cache?) somehow shows the same page. You can still use <a> tags to make it work, but yet I'm wondering if this is intended.

@madfcat
Copy link

madfcat commented Sep 28, 2023

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.

@sahanxdissanayake
Copy link

This issue still persists

@picozzimichele
Copy link

same here, revalidatePath() for dynamic routes not working

using "next": "13.5.4"

@khristovv
Copy link

khristovv commented Oct 12, 2023

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

@wencakisa
Copy link

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

@picozzimichele
Copy link

picozzimichele commented Oct 12, 2023

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

@maksimgorodilov
Copy link

Hello,
I'm using 13.5.4 version

Getting the same problem as people above.
app revalidatePath does not work, but pages res.revalidatePath does work.

I have a simple page in /app/static-page/page.tsx

export default () => {
    return (
        <div>Hello {Date()}</div>
    );
}

Then I have app revalidate route in /app/api/update-static-page/route.ts

import {revalidatePath} from "next/cache";
export async function GET() {
    revalidatePath('/static-page', "page");
    return Response.json({ revalidated: true })
}

And I have pages revalidate route in /pages/api/revalidate.ts

import { NextApiHandler } from "next";

const handler: NextApiHandler = async (req, res) => {
    try {
        await res.revalidate('/static-page');
        return res.json({ revalidated: true });
    } catch (err) {
        return res.status(500).send("Error revalidating");
    }
};

export default handler;

Then I run with rm -rf ./.next && npm run build && npm run start (clearing .next, just to make sure there is no conflicts from previous builds)

Then if I call app revalidate route, the static page does not change, but if I call pages revalidate route, it does change.

@github-actions
Copy link
Contributor

This closed issue has been automatically locked because it had no new activity for 2 weeks. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 28, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: app App directory (appDir: true) bug Issue was opened via the bug report template. linear: next Confirmed issue that is tracked by the Next.js team. locked
Projects
None yet
Development

Successfully merging a pull request may close this issue.