nextjs - 💡(How to fix) Fix 16.3.0-canary.2: notFound() returns HTTP 200 when dynamic route segment has sibling loading.tsx [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#93239Fetched 2026-04-26 05:05:27
View on GitHub
Comments
1
Participants
2
Timeline
4
Reactions
0
Timeline (top)
closed ×1commented ×1labeled ×1locked ×1

Error Message

  • The boundary swallows the notFound() error and renders the not-found.tsx tree, but by that point res.statusCode = 404 cannot reach the wire.

Root Cause

Any production app with streaming enabled (i.e., loading.tsx present anywhere in the segment tree) has to implement a middleware/proxy-layer 404 shim that does its own existence check against the data layer before the route renders, or live with HTTP 200 for missing detail pages. We ended up shipping that shim for a registry of dashboard detail routes:

https://github.com/holden-cgk/cgk-dashboard/blob/main/src/proxy.ts

…which duplicates work the page.tsx already does on valid loads. A framework-native fix (e.g., persisting notFound() to the response status from inside the boundary even when the response is streamed, or buffering the status line until the boundary settles) would let us retire the shim and avoid the duplicate round trip.

Fix Action

Fix / Workaround

Tracing the code path on [email protected]:

  • The dynamic segment renders inside an HTTPAccessFallbackBoundary (auto-injected when not-found.tsx exists in the segment).
  • When loading.tsx exists, the segment is wrapped in a Suspense boundary and Fizz starts streaming the response shell as soon as it can. Headers (including the status line) flush before the boundary catches notFound().
  • The boundary swallows the notFound() error and renders the not-found.tsx tree, but by that point res.statusCode = 404 cannot reach the wire.
  • Removing loading.tsx from the segment is the only framework-level workaround — but that disables streaming UX for the whole segment subtree.
  • export const dynamic = 'force-dynamic' alone does not flip this branch.

Workarounds

Code Example

Operating System:
  Platform: linux
  Arch: x64
Binaries:
  Node: v24.14.1
Relevant Packages:
  next: 16.3.0-canary.2
  react: 19.2.5
  react-dom: 19.2.5
  pnpm: 10.33.0

---

import { notFound } from "next/navigation";

export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  if (id !== "real") notFound();
  return <h1>ok</h1>;
}

---

export default function NotFound() {
  return <h1>not found</h1>;
}

---

export default function Loading() {
  return null;
}

---

git clone https://github.com/holdenjrussell/next-93008-repro
cd next-93008-repro
pnpm install
pnpm build
pnpm start --port 3199
curl -sI http://localhost:3199/missing

---

HTTP/1.1 200 OK
Vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
X-Powered-By: Next.js
Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
Content-Type: text/html; charset=utf-8

---

HTTP/1.1 404 Not Found
RAW_BUFFERClick to expand / collapse

Verify canary release

  • I verified that the issue exists in the latest Next.js canary release ([email protected])

Provide environment information

Operating System:
  Platform: linux
  Arch: x64
Binaries:
  Node: v24.14.1
Relevant Packages:
  next: 16.3.0-canary.2
  react: 19.2.5
  react-dom: 19.2.5
  pnpm: 10.33.0

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

Dynamic Routes, Linking and Navigating, Not Found

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

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

Link to the code that reproduces this issue

https://github.com/holdenjrussell/next-93008-repro

The three repro files (canary [email protected], scaffolded with create-next-app@canary):

app/[id]/page.tsx

import { notFound } from "next/navigation";

export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  if (id !== "real") notFound();
  return <h1>ok</h1>;
}

app/[id]/not-found.tsx

export default function NotFound() {
  return <h1>not found</h1>;
}

app/[id]/loading.tsx

export default function Loading() {
  return null;
}

To Reproduce

git clone https://github.com/holdenjrussell/next-93008-repro
cd next-93008-repro
pnpm install
pnpm build
pnpm start --port 3199
curl -sI http://localhost:3199/missing

Actual response:

HTTP/1.1 200 OK
Vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
X-Powered-By: Next.js
Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
Content-Type: text/html; charset=utf-8

Expected response:

HTTP/1.1 404 Not Found

Current vs. Expected behavior

Actual: the rendered body is the correct not-found.tsx content, but HTTP status is 200 OK.

Expected: HTTP status is 404 Not Found (regardless of whether the response is streamed). SEO crawlers and state-matrix audits treat the current behavior as a live page.

Tracing the code path on [email protected]:

  • The dynamic segment renders inside an HTTPAccessFallbackBoundary (auto-injected when not-found.tsx exists in the segment).
  • When loading.tsx exists, the segment is wrapped in a Suspense boundary and Fizz starts streaming the response shell as soon as it can. Headers (including the status line) flush before the boundary catches notFound().
  • The boundary swallows the notFound() error and renders the not-found.tsx tree, but by that point res.statusCode = 404 cannot reach the wire.
  • Removing loading.tsx from the segment is the only framework-level workaround — but that disables streaming UX for the whole segment subtree.
  • export const dynamic = 'force-dynamic' alone does not flip this branch.

Prior art

  • vercel/next.js#93008 — same bug, filed against 16.2.3, auto-closed by the no-public-repro bot and then locked. Refiling per the locked-issue close note. The previous issue had the same repro inline; this one provides the public repo link the template asks for.
  • vercel/next.js#76474 — closed, same symptom on 14.2.0 (loading.tsx + dynamic route).
  • vercel/next.js#45801 — closed, same symptom on 13.1 (loading.tsx anywhere in the segment tree).
  • vercel/next.js#62228 — open, adjacent (404 page is not SSR'd).

Why this matters

Any production app with streaming enabled (i.e., loading.tsx present anywhere in the segment tree) has to implement a middleware/proxy-layer 404 shim that does its own existence check against the data layer before the route renders, or live with HTTP 200 for missing detail pages. We ended up shipping that shim for a registry of dashboard detail routes:

https://github.com/holden-cgk/cgk-dashboard/blob/main/src/proxy.ts

…which duplicates work the page.tsx already does on valid loads. A framework-native fix (e.g., persisting notFound() to the response status from inside the boundary even when the response is streamed, or buffering the status line until the boundary settles) would let us retire the shim and avoid the duplicate round trip.

Workarounds

  • Remove loading.tsx from the segment — disables streaming, restores 404 — but loses the loading UX.
  • Intercept in middleware/proxy: validate the id and do a SELECT 1 existence check before the render starts. Real 404s, but extra round trip and per-route registry to maintain.
  • export const dynamic = 'force-dynamic' alone does not fix this.

Happy to contribute a PR if there is interest in landing a framework-level fix — would appreciate a pointer on whether this is considered tractable in the streaming codepath before investing the time.

extent analysis

TL;DR

The most likely fix for the issue is to remove the loading.tsx file from the segment or implement a middleware/proxy-layer 404 shim to handle the existence check.

Guidance

  • The issue is caused by the loading.tsx file in the segment, which enables streaming and prevents the notFound() error from setting the HTTP status to 404.
  • To verify the issue, run the provided reproduction steps and check the HTTP response status code.
  • A possible workaround is to remove the loading.tsx file, but this will disable streaming and lose the loading UX.
  • Another workaround is to implement a middleware/proxy-layer 404 shim to validate the id and perform an existence check before the render starts.

Example

No code example is provided as the issue is more related to the framework's behavior and configuration.

Notes

The issue is specific to the Next.js framework and its streaming feature. The provided workarounds may have trade-offs, such as disabling streaming or adding extra round trips.

Recommendation

Apply a workaround, such as removing the loading.tsx file or implementing a middleware/proxy-layer 404 shim, as a framework-level fix may require significant changes to the streaming codepath.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

nextjs - 💡(How to fix) Fix 16.3.0-canary.2: notFound() returns HTTP 200 when dynamic route segment has sibling loading.tsx [1 comments, 2 participants]