Skip to content

Cache Components causes fetch to hang #86662

@gaearon

Description

@gaearon

Link to the code that reproduces this issue

https://github.com/gaearon/next-bug-repro

To Reproduce

  1. Start https://github.com/gaearon/next-bug-repro
  2. Open http://localhost:3000/

Current vs. Expected behavior

Current behavior: it hangs

Expected behavior: it doesn't hang

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 23.6.0: Fri Nov 15 15:12:52 PST 2024; root:xnu-10063.141.1.702.7~1/RELEASE_ARM64_T6031
  Available memory (MB): 131072
  Available CPU cores: 16
Binaries:
  Node: 22.20.0
  npm: 10.9.3
  Yarn: 1.22.22
  pnpm: 10.18.1
Relevant Packages:
  next: 16.1.0-canary.4 // Latest available version is detected (16.1.0-canary.4).
  eslint-config-next: N/A
  react: 19.2.0
  react-dom: 19.2.0
  typescript: 5.9.3
Next.js Config:
  output: N/A

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

cacheComponents

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

next dev (local)

Additional context

This is in the repo already, but I'll briefly state the problem.

First, enable Cache Components.

Then, suppose we have page.ts like this:

async function cachedFn(): Promise<void> {
  "use cache";
  await new Promise((resolve) => setTimeout(resolve, 100));
}

export default async function Page() {
  await cachedFn();
  return <p>page content</p>;
}

And layout.ts like this:

const pending = new Map<string, Promise<any>>();

async function dedup(key: string): Promise<any> {
  let existingPromise;
  while ((existingPromise = pending.get(key))) {
    console.log("await existing");
    return await existingPromise;
  }

  console.log("starting");
  const promise = fetch("https://httpbin.org/get")
    .then((res) => {
      return res.json();
    })
    .finally(() => {
      console.log("done");
      pending.delete(key);
    });

  pending.set(key, promise);
  return promise;
}

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  await dedup("test");
  return (
    <html>
      <body>{children}</body>
    </html>
  );
}

The Map for pending Promises is something I extracted from a third-party OAuth client I'm using so I don't really have control over it. It seems designed to ensure serial execution. I suspect this is a reasonable pattern that might show up in different places so I wouldn't expect Next to break on it. If I'm not mistaken, the original piece of code (from which Claude extracted this minimal repro) was here (cc @matthieusieben in case he can provide more context on the requirement of OAuth flow).

The logs I see on start:

starting
await existing

So basically:

  1. The initial fetch runs but never completes (why?)
  2. The pending Promise stays in the map
  3. Any future attempt (maybe the first one was a prerender?) waits for that Promise forever

The most frustrating/surprising thing is that removing 'use cache' from the layout.ts (which intuitively feels completely unrelated to this code!) "fixes" the issue in page.ts.

My questions are:

  1. How to make it work with the original code (surely we can't expect the ecosystem to remove these types of locks from some flows)
  2. Is it a bug that mere presence of "use cache" on a completely different function causes this to break?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions