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:
…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 next@16.2.3 (also reproduces on 16.3.0-canary.2):
app-render.js:1894 — continueFizzStream 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
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
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 theinvalid linklabel even though the linked repo contains the full, runnable, three-file repro.)To Reproduce
In the StackBlitz / repo:
The repo contains three files:
app/[id]/page.tsxapp/[id]/not-found.tsxapp/[id]/loading.tsx/missingreturns:…with the rendered body being the correct
not-found.tsxcontent./realcorrectly returns 200. The 404 status is silently swallowed.Current vs. Expected behavior
Actual: any id other than
realrenders thenot-found.tsxtree 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
next@16.2.3(also reproduces on16.3.0-canary.2):app-render.js:1894—continueFizzStreamflushes the response headers as soon as the shell renders.notFound(). The nearestHTTPAccessFallbackBoundary(auto-injected aroundnot-found.tsx) catches the throw and renders the not-found tree inline.app-render.js:1949that would setres.statusCode = 404never runs.export const dynamic = 'force-dynamic'does NOT flipgenerateStaticHTMLoff this branch.loading.tsx(which is what triggers Suspense wrapping) is the only framework-level workaround.Prior art
loading.tsx+ dynamic route).loading.tsxanywhere).generateMetadata).invalid linkbot on 2026-04-19.router.refresh()eagerly refetches every in-viewport<Link>in v16 (was lazy in v15) — ~200x peak ISR Writes in production #93210, Turbopack dev on macOS hits Watchpack fs.watch EMFILE on ancestor directories and app subtrees #93175, Turbopack crashes inpages/_app.tsxwhennext/fontis imported #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.tsxpresent 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.tsxalready does on valid loads. A framework-native path (e.g., "if a component throwsnotFound()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
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
next@16.3.0-canary.2).