nextjs - 💡(How to fix) Fix 16.2.3: 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#93008Fetched 2026-04-20 11:58:27
View on GitHub
Comments
1
Participants
2
Timeline
4
Reactions
0
Timeline (top)
closed ×1commented ×1labeled ×1locked ×1

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

This forces any app with streaming enabled (loading.tsx present anywhere in the segment tree) to implement a middleware/proxy-layer 404 shim that does its own existence check against the DB before the route renders. We ended up writing 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 round trip.

Fix Action

Fix / Workaround

Tracing the code path in [email protected]:

  • app-render.js:1894continueFizzStream flushes response headers as soon as the shell renders.
  • Server component calls notFound() → the nearest HTTPAccessFallbackBoundary (dropped in by the framework around not-found.tsx) catches it and renders the not-found.tsx 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' by itself does not flip generateStaticHTML off this branch; removing loading.tsx (which is what triggers Suspense wrapping) is the only workaround at the framework level.

Workarounds

Code Example

Operating System:
  Platform: linux
  Arch: x64
Binaries:
  Node: v24.13.1
Relevant Packages:
  next: 16.2.3
  react: (19.x)

---

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

---

pnpm create next-app@16.2.3 repro --ts --app --no-tailwind --no-src --no-turbopack --import-alias "@/*"
cd repro && add the 3 files above
pnpm build && pnpm start
curl -sI http://localhost:3000/missing

---

HTTP/1.1 200 OK

---

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

Provide environment information

Operating System:
  Platform: linux
  Arch: x64
Binaries:
  Node: v24.13.1
Relevant Packages:
  next: 16.2.3
  react: (19.x)

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), Vercel (Deployed)

Link to the code that reproduces this issue

Inline minimal repro (3 files, [email protected] scaffold):

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

pnpm create [email protected] repro --ts --app --no-tailwind --no-src --no-turbopack --import-alias "@/*"
cd repro && add the 3 files above
pnpm build && pnpm start
curl -sI http://localhost:3000/missing

Actual response:

HTTP/1.1 200 OK

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 streaming). SEO crawlers and state-matrix audits treat this as a live page.

Tracing the code path in [email protected]:

  • app-render.js:1894continueFizzStream flushes response headers as soon as the shell renders.
  • Server component calls notFound() → the nearest HTTPAccessFallbackBoundary (dropped in by the framework around not-found.tsx) catches it and renders the not-found.tsx 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' by itself does not flip generateStaticHTML off this branch; removing loading.tsx (which is what triggers Suspense wrapping) is the only workaround at the framework level.

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)

Re-filing against 16.2.3 because the prior dynamic-route issue was closed without a behavioral fix and the symptom is still present on current stable.

Why this matters

This forces any app with streaming enabled (loading.tsx present anywhere in the segment tree) to implement a middleware/proxy-layer 404 shim that does its own existence check against the DB before the route renders. We ended up writing 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 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. Expensive (extra round trip) but returns a real 404.
  • export const dynamic = 'force-dynamic' alone does not fix this.

Happy to contribute a PR if there's 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 issue can be worked around by removing the loading.tsx file or implementing a middleware/proxy-layer 404 shim to handle the existence check.

Guidance

  • The root cause of the issue is that the notFound() function is being swallowed by the HTTPAccessFallbackBoundary and not propagating the 404 status code to the response.
  • To verify the issue, run the provided reproduction steps and check the HTTP status code of the response.
  • A potential workaround is to remove the loading.tsx file, which will disable streaming and restore the 404 status code.
  • Another workaround is to implement a middleware/proxy-layer 404 shim, such as the one provided in the proxy.ts file, to handle the existence check and return a 404 status code.

Example

// Remove loading.tsx to disable streaming and restore 404 status code
// or implement a middleware/proxy-layer 404 shim, such as:
const proxy = async (req, res) => {
  const id = req.params.id;
  // Perform existence check against the DB
  const exists = await checkExistence(id);
  if (!exists) {
    res.statusCode = 404;
    res.end();
  } else {
    // Proceed with the render
  }
};

Notes

The issue is specific to the [email protected] version and is related to the streaming codepath. The provided workarounds may have trade-offs, such as losing the loading UX or introducing an extra round trip.

Recommendation

Apply the workaround of removing the loading.tsx file or implementing a middleware/proxy-layer 404 shim, as a framework-native fix is not currently available.

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