Skip to content

notFound() returns 200 instead of 404 when a sibling route has dynamicParams: true #99318

Description

@afaqjutt786470-ops

Link to the code that reproduces this issue

https://github.com/afaqjutt786470-ops/nextjs-nested-notfound-200-bug

To Reproduce

  1. Clone the linked reproduction and npm install
  2. npm run build && npm run start
  3. curl -I http://localhost:3000/brands/does-not-exist -> HTTP 200 (bug; expected 404)
  4. curl -I http://localhost:3000/laptops/does-not-exist -> HTTP 200 (bug; this is the sibling dynamicParams: true route itself, also affected)
  5. curl -I http://localhost:3000/does-not-exist -> HTTP 404 (control; a single top-level dynamic segment with no sibling dynamicParams: true route anywhere in the app is unaffected)
  6. curl -I http://localhost:3000/brands/apple -> HTTP 200 (known param, unaffected either way)

The repro's README documents which combination of ingredients is required (app/brands/[brand]/page.tsx with dynamicParams:false + notFound(), PLUS a root app/loading.tsx, PLUS a sibling app/[category]/[slug]/page.tsx with dynamicParams:true elsewhere in the app -- the sibling route never needs to be requested, its mere presence in the build is sufficient). Removing any one of these three ingredients makes /brands/does-not-exist correctly return 404 again; this was verified by testing each reduced combination individually (documented in the repro README).

Current vs. Expected behavior

When visiting a non-existent brand page like /brands/does-not-exist, I expected the page to return a proper 404 Not Found status. Instead, the page returns HTTP 200 with the not-found UI content, which means Google and other crawlers treat it as a valid page instead of a missing one. This happens specifically when a sibling dynamic route elsewhere in the app uses dynamicParams: true, even though that sibling route is never directly requested.

Provide environment information

Operating System:
  Platform: win32
  Arch: x64
  Version: Windows 11 Pro
  Available memory (MB): 8007
  Available CPU cores: 8
Binaries:
  Node: 20.19.5
  npm: 10.8.2
  Yarn: N/A
  pnpm: N/A
Relevant Packages:
  next: 16.4.0-canary.50 // Latest available version is detected (16.4.0-canary.50).
  eslint-config-next: N/A
  react: 19.3.0
  react-dom: 19.3.0
  typescript: 5.9.3
Next.js Config:
  output: N/A

Also independently verified on next@16.3.6 (latest stable release) with the same result -- see the linked reproduction's README for the exact version-switch steps used.

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

Dynamic Routes, Loading UI and Streaming, Not Found

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

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

Additional context

This was originally found on a production Next.js 16.3.4 app deployed to Vercel, where an unmatched param under a dynamicParams: false route (e.g. /brands/<unknown-brand>) reliably returned HTTP 200 with the correct not-found.tsx content instead of 404 -- both on that Vercel deployment and via next start locally with a freshly-built .next directory (ruling out a stale-cache artifact). The app was then upgraded to 16.3.6, and the bug was still present, confirming it independently of the specific canary/stable build.

The attached minimal reproduction isolates the same behavior down to three ingredients (documented in its README): a route with dynamicParams: false that calls notFound(), a root app/loading.tsx, and a sibling route anywhere in the app with dynamicParams: true (that sibling route never has to be requested -- its mere presence in the build is enough). Verified reproducing on both next@16.3.6 and next@canary (16.4.0-canary.50) via next build && next start locally.

Debugging note: running the affected route locally with NEXT_PRIVATE_DEBUG_CACHE=1 shows a FileSystemCache: set <path> log line for the unmatched param's path, as if it were a normal cacheable 200 page -- suggesting the 404 status set by notFound() during the on-demand fallback render isn't being carried through to what gets written to the incremental cache for that entry.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Dynamic RoutesRelated to dynamic routes.Loading UI and StreamingRelated to loading UI (loading.tsx) and streaming.Not FoundRelated to the not-found.tsx file or the notFound() function.

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions