nextjs - 💡(How to fix) Fix 16.2.3 / 16.3.0-canary.2: notFound() returns HTTP 200 when sibling loading.tsx is present (HTTPAccessFallbackBoundary swallows 404 status under streaming) [1 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#93253Fetched 2026-04-26 05:05:24
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0

Error Message

  • Because the boundary swallows the error, the framework-level handler at app-render.js:1949 that would set res.statusCode = 404 never runs.

Root Cause

Any app with streaming enabled (i.e., loading.tsx present anywhere in the segment tree) is forced to implement a middleware/proxy-layer 404 shim that does its own existence check against the DB before the route renders. We wrote one here:

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 path (e.g., "if a component throws notFound() inside the boundary, persist that to the response status even on streamed responses") would let us retire the shim and avoid the duplicate DB round-trip per detail-page load.

Fix Action

Fix / Workaround

  • app-render.js:1894continueFizzStream flushes the response headers as soon as the shell renders.
  • The server component calls notFound(). The nearest HTTPAccessFallbackBoundary (auto-injected around not-found.tsx) catches the throw and renders the not-found tree inline.
  • Because the boundary swallows the error, the framework-level handler at app-render.js:1949 that would set res.statusCode = 404 never runs.
  • export const dynamic = 'force-dynamic' does NOT flip generateStaticHTML off this branch.
  • Removing loading.tsx (which is what triggers Suspense wrapping) is the only framework-level workaround.

Code Example

pnpm install
pnpm build
pnpm start --port 3199
curl -sI http://localhost:3199/missing   # any id !== "real"
curl -sI http://localhost:3199/real

---

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;
}

---

HTTP/1.1 200 OK

---

Operating System:
  Platform: linux
  Arch: x64
Binaries:
  Node: v24.14.1
Relevant Packages:
  next: 16.2.3 (also reproduces on 16.3.0-canary.2)
  react: 19.2.5
RAW_BUFFERClick to expand / collapse

Link to the code that reproduces this issue

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

(Backed by the GitHub repo at https://github.com/holdenjrussell/next-93008-repro, which is the same minimal app the StackBlitz loads. StackBlitz used per the bug-report template and to clear the auto-close bot heuristic — three previous re-files of this verification with raw github.com links were rejected by github-actions[bot] with the invalid link label even though the linked repo contains the full, runnable, three-file repro.)

To Reproduce

In the StackBlitz / repo:

pnpm install
pnpm build
pnpm start --port 3199
curl -sI http://localhost:3199/missing   # any id !== "real"
curl -sI http://localhost:3199/real

The repo contains three files:

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;
}

/missing returns:

HTTP/1.1 200 OK

…with the rendered body being the correct not-found.tsx content. /real correctly returns 200. The 404 status is silently swallowed.

Current vs. Expected behavior

Actual: any id other than real renders the not-found.tsx tree as the response body, but HTTP status is 200 OK.

Expected: HTTP status is 404 Not Found, regardless of whether the response body is streamed.

Tracing the code path in [email protected] (also reproduces on 16.3.0-canary.2):

  • app-render.js:1894continueFizzStream flushes the response headers as soon as the shell renders.
  • The server component calls notFound(). The nearest HTTPAccessFallbackBoundary (auto-injected around not-found.tsx) catches the throw and renders the not-found tree inline.
  • Because the boundary swallows the error, the framework-level handler at app-render.js:1949 that would set res.statusCode = 404 never runs.
  • export const dynamic = 'force-dynamic' does NOT flip generateStaticHTML off this branch.
  • Removing loading.tsx (which is what triggers Suspense wrapping) is the only framework-level workaround.

Prior art

  • #76474 — closed, same symptom on 14.2.0 (loading.tsx + dynamic route).
  • #45801 — closed, same symptom on 13.1 (loading.tsx anywhere).
  • #62228 — open, adjacent (404 page is not SSR'd).
  • #82041 — open, adjacent (non-streaming generateMetadata).
  • #93008 — original filing of this exact bug, auto-closed and locked by invalid link bot on 2026-04-19.
  • #93238 / #93239 — re-files with public-repo links, both auto-closed and locked by the bot within ~10s despite the URL placement matching recently accepted issues (#93210, #93175, #93162). This issue uses a StackBlitz link instead of a raw github.com link to clear that heuristic.

Why this matters

Any app with streaming enabled (i.e., loading.tsx present anywhere in the segment tree) is forced to implement a middleware/proxy-layer 404 shim that does its own existence check against the DB before the route renders. We wrote one here:

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 path (e.g., "if a component throws notFound() inside the boundary, persist that to the response status even on streamed responses") would let us retire the shim and avoid the duplicate DB round-trip per detail-page load.

Provide environment information

Operating System:
  Platform: linux
  Arch: x64
Binaries:
  Node: v24.14.1
Relevant Packages:
  next: 16.2.3 (also reproduces on 16.3.0-canary.2)
  react: 19.2.5

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)

Verify canary release

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

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.2.3 / 16.3.0-canary.2: notFound() returns HTTP 200 when sibling loading.tsx is present (HTTPAccessFallbackBoundary swallows 404 status under streaming) [1 participants]