vercel / next.js

The React Framework
https://nextjs.org
MIT License
125.77k stars 26.85k forks source link

revalidatePath with middleware doesn't work on multi-tenant project #59825

Open sergiolamorda opened 9 months ago

sergiolamorda commented 9 months ago

Link to the code that reproduces this issue

https://github.com/sergiolamorda/nextjs-issue-revalidatePath-multitenant

To Reproduce

First, add two domains to your local hostname, for example:

127.0.0.1   test1.local
127.0.0.1   test2.local

hen, execute the build and start commands:

npm run build
npm run start

Next, open two different tabs and navigate to http://test1.local:3000/test and http://test2.local:3000/test respectively.

Now, make a POST request to http://localhost:3000/api/revalidate, which will execute revalidatePath('/test2.local/test'). If you reload the previous tab, the content does not update.

Finally, make a POST request to http://localhost:3000/api/revalidate2, which will execute revalidatePath('/test'). If you reload the previous tabs, both will show updated content.

Current vs. Expected behavior

We're working on a multi-tenant project that employs middleware to rewrite requests, converting the host into a slug.

In Next.js 12, you can perform on-demand revalidation in the API using res.revalidate(), which only invalidates the cache for the domain where the request was made.

However, after migrating to Next.js 14, we encountered issues when trying to invalidate the cache with revalidatePath; it doesn't seem to work as expected.

Attempting revalidatePath('/domain/test') doesn't take the middleware into account and fails to produce any effect.

On the other hand, using revalidatePath('/test') results in invalidating the /test path across all domains.

Verify canary release

Provide environment information

Operating System:
  Platform: win32
  Arch: x64
  Version: Windows 11 Pro
Binaries:
  Node: 20.9.0
  npm: N/A
  Yarn: N/A
  pnpm: N/A
Relevant Packages:
  next: 14.0.4
  eslint-config-next: N/A
  react: 18.2.0
  react-dom: 18.2.0
  typescript: N/A
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

Data fetching (gS(S)P, getInitialProps), Middleware / Edge (API routes, runtime)

Additional context

No response

rogerbandera commented 9 months ago

We have the same issue, we cannot update version. Hopefully it will be solved as soon as possible...

sergiolamorda commented 9 months ago

Here's an example of what's happening to me. My route structure is as follows: [domain]/[[…slug]]

I execute revalidatePath('/test2.local') Browser Result
http://test1.local/test1.local Keeps cache
http://test1.local/test2.local Invalidates cache
http://test2.local/test1.local Keeps cache
http://test2.local/test2.local Invalidates cache
If I execute revalidatePath('/test2.local/test2.local') Browser Result
http://test1.local/test1.local Keeps cache
http://test1.local/test2.local Keeps cache
http://test2.local/test1.local Keeps cache
http://test2.local/test2.local Keeps cache

It should be noted that the middleware rewrites the parameters so that the browser correctly interprets the route structure of Next.js. url.pathname = '/${hostname}${pathname}'

We are currently using version 14.0.4 I don't understand what could be happening, it seems like there might be a bug. What do you think?

eric-burel commented 9 months ago

I confirm I can reproduce OP issue and it's super unsettling, both the fact that the first revalidate on "/localhost/test" doesn't work while having a correct path, and the revalidate2 does work on "/test" despite having a wrong path.

I've also tried with [slug] instead of [...slug], and tweaked the code to revalidate directly on localhost.

This feels like as some point the URL users sees is used a cache key, instead of the underlying path in the Next.js app. But surprising because this seems to affect the server cache, since the value is also not updated when opening the page with another browser.

sergiolamorda commented 8 months ago

I have delved a bit more into this error and I am sharing it in case it helps.

I have found that the tags generated for the pages are created from the request urlPathname, and this value does not take into account the rewrite of the middleware.

https://github.com/vercel/next.js/blob/bb2aaf74e86049e89e0c10b7fca099c3a33443f5/packages/next/src/server/future/route-modules/app-route/module.ts#L265

https://github.com/vercel/next.js/blob/cf0f090fca6183e5c3d525a339d34b9543519126/packages/next/src/server/lib/patch-fetch.ts#L124

This causes the tags to be associated with the user's request and not with the actual structure of the project.

LouisCuvelier commented 8 months ago

I was facing this kind of issue also. I found a workaround explained here : https://github.com/amannn/next-intl/issues/846 Maybe it could work for you ?

B33fb0n3 commented 7 months ago

The "solution" that I am currently use is by using unstable_cache. With that, you are able to control the data and also the pages, even when the pages are "forced-static". You can take a look at this repository, that shows how I resolved it: https://github.com/B33fb0n3/revalidaterewrites

To test it, Go to /helpi and /helpi?host=someotherhost.com

Click the "revalidate" Button on the page "/helpi" (without query params). You see, that "somehost.com" gets revalidated and "someotherhost.com" not. Even when they are rewritten.

I found this in the vercel platform example: https://github.com/vercel/platforms/

reinvanhaaren commented 7 months ago

I have the same issue. I'm rewriting to different subfolders by mapping the request header host to /${host}. The UI is not showing fresh data after revalidating.

When I call a server function from a server component containing revalidateTag or revalidatePath, the cache is invalidated (I see the new data in the .next/cache/fetch-cache/ folder), the unstable_cache function is executed on revalidation (I see my console.log on the server), but the UI won't reflect the updated data in the same roundtrip when I'm on a route rewritten by middleware. I have to perform a hard refresh of my browser window to see the updated data on my page.

reinvanhaaren commented 7 months ago

After a few hours of debugging I noticed i alter the headers in my NextResponse.rewrite. When I comment this line out, the data is reflected in the way you expect it to work on revalidation.

Turns out I added the user's request headers to the response 🤦🏼‍♂️

   // Add a header to the response to indicate path in layout
-  const responseHeaders = new Headers(request.headers);
+  const responseHeaders = new Headers();
   responseHeaders.set("x-frontend-path", path);

   const response = NextResponse.rewrite(
     new URL(`/${path}${fullPath === "/" ? "" : fullPath}`, request.url),
     { headers: responseHeaders },
   );
DzTheRage commented 7 months ago

I ended up just going with revalidateTag() instead of revalidatePath() to get around this issue.

Zonalds commented 5 months ago

Using revalidateTag works fine. You can assign dynamic tag to force revalidate for individual tenant.

Something like this:

async function getSettings(domain) {
    const token = process.env.API_TOKEN
    const fetchUrl = `${process.env.API_URL}/api/settings?domain=${domain}`
    const settingsData = await fetch(`${fetchUrl}`, {
        headers: {
            Authorization: `Bearer ${token}`,
        },
        cache: "force-cache",
        next: { tags: [`site-settings-${domain}`] }
    }).then((res) => res.json())
    console.log({ settingsData })
    return settingsData?.data
}