vercel / next.js

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

Fetch request in Not Found page (404) is unexpectedly called multiple times on every page request #61765

Open orinokai opened 9 months ago

orinokai commented 9 months ago

Link to the code that reproduces this issue

https://github.com/orinokai/test-next-404-fetch

To Reproduce

  1. Observe the count (persisted in a backend database) at https://kounter.vercel.app/get/h3ZJ97ra9Q
  2. Start the repro in dev mode (or with next start or deployed to Vercel)
  3. Observe that the count has incremented by an arbitrary amount
  4. Request a page, e.g. /posts/post-1
  5. Observe that the count has incremented, again by a random number (the upper limit seems to be the number of pages on the site?)

Current vs. Expected behavior

The counter is incremented via a fetch request in the Not Found page. The fetch request should only be made when the Not Found page is rendered, but is currently seen to happen multiple times during build and at runtime when requesting any page on the site (even statically rendered pages).

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: 20.9.0
  npm: 10.1.0
  Yarn: 1.22.21
  pnpm: 8.9.0
Relevant Packages:
  next: 14.1.0
  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)

App Router

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

next dev (local), next build (local), next start (local), Vercel (Deployed), Other (Deployed)

Additional context

This bug seems to occur with or without the { cache: "no-store" } on the fetch call in the Not Found page

coffeecupjapan commented 9 months ago

Hi. I read the source code of Next.js and find quick fix.

Firstly explain the reason why this happens. In Next.js, server components including not-found.jsx are always being processed by the React server component's function "renderToReadableStream".

https://github.com/vercel/next.js/blob/0114c2cb24c3b146ac323d6e015a3dc3dbee2e65/packages/next/src/server/app-render/create-server-components-renderer.tsx#L21

In this function, all the bundled server components including not-found.jsx ( not-found-jsx are bundled in build phase whether or not the user is seeing not-found page or not! ) are processed by Renderer of React Server and create JSON like formatted data later send to browser. In this process, Renderer of React Server must understand what to render for serverside and getData is called in server-side.

I am seeking how to fix this for the source code of Next.js now, However there is a quick fix.

Since not-found.jsx is server-component, it is render at server. So you can just use client component to bypass this problem. Here is the code. I hope it help you.

app/not-found.jsx

import NotFoundImpl from "@/src/not-found-impl";

export default async function NotFound() {
  return (
    <NotFoundImpl/>
  )
}

src/non-found-impl.jsx

'use client'

import React, {useEffect, useState} from "react";

export default function NotFoundImpl() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    fetch(`https://kounter.vercel.app/hit/h3ZJ97ra9Q`, { cache: "no-store" })
    .then((result) => result.json())
    .then((result) => {
      setCount(result.count);
    })
  }, [])
  return (
    <>
      <h1>Hit</h1>
      <p>The API has been hit {count} times</p>
    </>
  )
}

or maybe you have to add

module.exports = {
    reactStrictMode: false,
}

in next.config.js to prevent useEffect from being called twice.

I will keep seeking other fix in source codes. Thank you!

orinokai commented 9 months ago

thanks @coffeecupjapan - that would fix the problem in this case, but there are other cases where it is important to be able to fetch on the server. i appreciate the suggestion though

coffeecupjapan commented 9 months ago

@orinokai Hi. I try to fix this issue in source code. And codes below delete not-found.js from bundle and fix issue above.

https://github.com/coffeecupjapan/next.js/commit/497e87fe8f3633dded45c8928b922b6f53a7625e

However, it could not pass test cases that throw notFound() in server or throw notFound() in client navigation. I could not find way to revive deleted not-found.js in bundle in first case, and for second case I have totally no idea.

I hope any maintainer would tackle this issue.

tcrz commented 1 week ago

Hi. I read the source code of Next.js and find quick fix.

Firstly explain the reason why this happens. In Next.js, server components including not-found.jsx are always being processed by the React server component's function "renderToReadableStream".

https://github.com/vercel/next.js/blob/0114c2cb24c3b146ac323d6e015a3dc3dbee2e65/packages/next/src/server/app-render/create-server-components-renderer.tsx#L21

In this function, all the bundled server components including not-found.jsx ( not-found-jsx are bundled in build phase whether or not the user is seeing not-found page or not! ) are processed by Renderer of React Server and create JSON like formatted data later send to browser. In this process, Renderer of React Server must understand what to render for serverside and getData is called in server-side.

I am seeking how to fix this for the source code of Next.js now, However there is a quick fix.

Since not-found.jsx is server-component, it is render at server. So you can just use client component to bypass this problem. Here is the code. I hope it help you.

app/not-found.jsx

import NotFoundImpl from "@/src/not-found-impl";

export default async function NotFound() {
  return (
    <NotFoundImpl/>
  )
}

src/non-found-impl.jsx

'use client'

import React, {useEffect, useState} from "react";

export default function NotFoundImpl() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    fetch(`https://kounter.vercel.app/hit/h3ZJ97ra9Q`, { cache: "no-store" })
    .then((result) => result.json())
    .then((result) => {
      setCount(result.count);
    })
  }, [])
  return (
    <>
      <h1>Hit</h1>
      <p>The API has been hit {count} times</p>
    </>
  )
}

or maybe you have to add

module.exports = {
    reactStrictMode: false,
}

in next.config.js to prevent useEffect from being called twice.

I will keep seeking other fix in source codes. Thank you!

im experiencing this issue even after making it a client component