Link to the code that reproduces this issue
https://github.com/afaqjutt786470-ops/nextjs-nested-notfound-200-bug
To Reproduce
- Clone the linked reproduction and
npm install
npm run build && npm run start
curl -I http://localhost:3000/brands/does-not-exist -> HTTP 200 (bug; expected 404)
curl -I http://localhost:3000/laptops/does-not-exist -> HTTP 200 (bug; this is the sibling dynamicParams: true route itself, also affected)
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)
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.
Link to the code that reproduces this issue
https://github.com/afaqjutt786470-ops/nextjs-nested-notfound-200-bug
To Reproduce
npm installnpm run build && npm run startcurl -I http://localhost:3000/brands/does-not-exist-> HTTP 200 (bug; expected 404)curl -I http://localhost:3000/laptops/does-not-exist-> HTTP 200 (bug; this is the siblingdynamicParams: trueroute itself, also affected)curl -I http://localhost:3000/does-not-exist-> HTTP 404 (control; a single top-level dynamic segment with no siblingdynamicParams: trueroute anywhere in the app is unaffected)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: falseroute (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 vianext startlocally with a freshly-built.nextdirectory (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: falsethat callsnotFound(), a rootapp/loading.tsx, and a sibling route anywhere in the app withdynamicParams: true(that sibling route never has to be requested -- its mere presence in the build is enough). Verified reproducing on bothnext@16.3.6andnext@canary(16.4.0-canary.50) vianext build && next startlocally.Debugging note: running the affected route locally with
NEXT_PRIVATE_DEBUG_CACHE=1shows aFileSystemCache: set <path>log line for the unmatched param's path, as if it were a normal cacheable 200 page -- suggesting the 404 status set bynotFound()during the on-demand fallback render isn't being carried through to what gets written to the incremental cache for that entry.