vercel / next.js

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

unstable_cache Value Not Revalidating in Edge Runtime within Suspense Boundary on Vercel #60336

Open exectx opened 8 months ago

exectx commented 8 months ago

Link to the code that reproduces this issue

https://github.com/senabi/next-suspense-cache-behavior

To Reproduce

The problem doesn't happen for a local build or development mode

  1. Visit this link: https://next-suspense-cache-behavior.vercel.app/.

  2. There's a problem with the route: https://next-suspense-cache-behavior.vercel.app/streaming-cache-bugged. It's supposed to fetch the current time and then fetch it again every 10 seconds. Even though the page waits 200ms before showing the time, it doesn't refresh the time after the first 10 seconds have passed.

  3. However, on the route: https://next-suspense-cache-behavior.vercel.app/streaming-cache-no-simulated-latency, without adding a delay (no 200ms wait time), the time updates correctly every 10 seconds.

  4. In contrast, the route: https://next-suspense-cache-behavior.vercel.app/streaming-node-runtime, which uses a different setup (Node.js runtime and a unstable_noStore), doesn't have this problem.

  5. Lastly, on this link: https://next-suspense-cache-behavior.vercel.app/streaming-cache-patch, with the same 200ms delay, the time does update every 10 seconds, but it needs another cache read from a different async component (without suspense boundary) for this to work.

In conclusion, it seems there's an issue with revalidating cached data every X seconds on the edge runtime with a delay. The Node.js runtime and including an async cache read don't have this problem.

Current vs. Expected behavior

Current Behavior:

Expected Behavior:

Here is a video demonstrating the cached value failing to revalidate and then revalidating https://github.com/vercel/next.js/assets/81214874/5865c9b4-c078-41a0-88a2-4b6caa406a60

Verify canary release

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 23.2.0: Wed Nov 15 21:53:18 PST 2023; root:xnu-10002.61.3~2/RELEASE_ARM64_T6000
Binaries:
  Node: 18.18.0
  npm: 9.8.1
  Yarn: N/A
  pnpm: 8.7.5
Relevant Packages:
  next: 14.0.5-canary.41
  eslint-config-next: 14.0.4
  react: 18.2.0
  react-dom: 18.2.0
  typescript: 5.3.3
Next.js Config:
  output: N/A

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

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

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

Vercel (Deployed), Other (Deployed)

Additional context

The problem happens when it's deployed in vercel, I also tried deploying on cloudflare and the behavior is the same

ATeals commented 8 months ago

I've had a similar problem. I've seen fetch on the Suspans boundary that it works as SSR, not ISR, in a vercel deployment environment with revalidate as false.

This error occurred during deployment, although it works as an ISR in a local build or development build environment.

(My prediction is that ISR's cache continues to miss.)

In my case, I downgraded from the next 14 version to the 13.4.16 version, so it worked normally.

exectx commented 8 months ago

@ATeals I can't get it to build with version 13.4.16 I get this error:

- info Creating an optimized production build  
- info Compiled successfully
- info Linting and checking validity of types  
- info Collecting page data ..Error: Invariant: incrementalCache missing in unstable_cache async ()=>{
    return new Date().toISOString();
}
    at unstable_cache (/Users/vni/code/personal/cloudflare-experiments/vercel-next-unstable-cache/.next/server/chunks/373.js:8446:15)
    at 264 (/Users/vni/code/personal/cloudflare-experiments/vercel-next-unstable-cache/.next/server/app/streaming-node-runtime/page.js:353:44)
    at Function.__webpack_require__ (/Users/vni/code/personal/cloudflare-experiments/vercel-next-unstable-cache/.next/server/webpack-runtime.js:25:42)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async collectGenerateParams (/Users/vni/code/personal/cloudflare-experiments/vercel-next-unstable-cache/node_modules/.pnpm/next@13.4.16_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/build/utils.js:826:17)

> Build error occurred
Error: Failed to collect page data for /streaming-node-runtime
    at /Users/vni/code/personal/cloudflare-experiments/vercel-next-unstable-cache/node_modules/.pnpm/next@13.4.16_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/build/utils.js:1156:15
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  type: 'Error'
}
- info Collecting page data . ELIFECYCLE  Command failed with exit code 1.
exectx commented 8 months ago

This is a much more minimal example that doesn't revalidate.

import { unstable_cache } from "next/cache";
import { Suspense } from "react";

export const runtime = "edge";

async function getLatestStory() {
  return fetch('https://hacker-news.firebaseio.com/v0/newstories.json', {
    next: {
      revalidate: 600
    }
  })
    .then(response => response.json())
    .then(async storyIds => {
      const randomIdx = Math.floor(Math.random() * storyIds.length);
      const storyId = storyIds[randomIdx];
      console.log({ storyId })
      return fetch(`https://hacker-news.firebaseio.com/v0/item/${storyId}.json`)
        .then(response => response.json())
        .then(story => {
          return (story.title) as string;
        })
    })
}

const cachedHackerNews = unstable_cache(
  getLatestStory,
  ['hacker-news'],
  { revalidate: 10 }
)

async function HackerNews() {
  const data = await cachedHackerNews();
  return <div>{data}</div>
}

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <Suspense fallback={<div>Loading...</div>}>
        <HackerNews />
      </Suspense>
    </main>
  )
}

repo: https://github.com/senabi/next-cache-bug-edge vercel link: https://next-cache-bug-edge.vercel.app

exectx commented 8 months ago

For anyone who has this problem the best solution without an additional cache read from unstable_cache would be to simply use nodejs runtime. If anyone deals with this on cloudflare, I'd appreciate the help.

exectx commented 7 months ago

error persists on next@14.1.1-canary.20

bonesoul commented 3 months ago

same issue here, any possible fixes?

Edit by maintainer bot: Comment was automatically minimized because it was considered unhelpful. (If you think this was by mistake, let us know). Please only comment if it adds context to the issue. If you want to express that you have the same problem, use the upvote 👍 on the issue description or subscribe to the issue for updates. Thanks!

markomitranic commented 3 months ago

Actually, it also happens when running locally via next build --experimental-build-mode=compile && next start so it should be very accessible to debug.

Here is an even more minimal example:

    const cachedMessageWithName = unstable_cache(
      async (name: string) => {
        await new Promise((resolve) => setTimeout(resolve, 4000));
        return `Hello ${name}`;
      },
      ["cachedMessageWithName"],
      { revalidate: 10 },
    );

Expectation is that it will revalidate every 10 seconds, but it never does.

I am not sure why this isn't getting the visibility it needs, it is quite a big issue. I thought I was crazy and lost a lot of time debugging it. Perhaps, until it gets queued we could add a notice to the docs at https://nextjs.org/docs/app/api-reference/functions/unstable_cache ?

exectx commented 2 months ago

It looks like the problem also affects fetch. Perhaps suspense breaks the time-based revalidation in the edge runtime. I tested this with a next@15.0.0-canary.19 vercel deployment.