nextjs - 💡(How to fix) Fix 16.3.0-canary.2: HTTPAccessFallbackBoundary swallows notFound() status code when dynamic route 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#93238Fetched 2026-04-26 05:05:28
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

  • Remove loading.tsx from the segment to restore the 404 status, but this will disable streaming and the loading UX.
  • Implement a middleware/proxy-layer 404 shim to validate the id and perform an existence check before the render starts, which will add an extra round trip but provide a real 404 response.
  • Consider contributing a PR to fix the issue at the framework level, which would involve persisting the notFound() status to the response even when the response is streamed.
  • Review the prior art issues (e.g., vercel/next.js#93008, vercel/next.js#76474) for more context and potential solutions.

Example

No code snippet 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 interaction with streaming and dynamic routes. The provided workarounds have trade-offs, and a framework-level fix would be ideal to resolve the issue without compromising the loading UX or adding extra round trips.

Recommendation

Apply a middleware/proxy-layer 404 shim as a workaround, as it provides a real 404 response and allows for the preservation of the loading UX, although it adds an extra round trip.

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: HTTPAccessFallbackBoundary swallows notFound() status code when dynamic route has sibling loading.tsx [1 comments, 2 participants]