skip to content

Why can a meta-framework serve a not-found page with a 200 status, and when does that happen?

level: middleimportance: must knowfreq 62%

answer

  1. headers leave before the body does
  2. router miss versus data miss
  3. declared too late to matter
  4. a prerendered file carries no status
  5. soft 404 is indexed and cached

basics

~20 s

The status ships with the response headers; the not-found page is only the body. If absence is discovered after the response commits, or the page is a prerendered file whose host sets no status, the two disagree.

solid answer

~50 s

A router miss is easy: nothing has rendered, so the framework answers 404 with its not-found page. The mismatch comes from the other case — a route that *did* match, whose data step then finds no record. Because the status line goes out with the headers, the not-found outcome has to be declared before the response is committed; declared late, you get a page that says "not found" over a 200. Rendering mode decides how much room you have. A per-request render can pick any status. A streamed route flushed its shell with 200 already. A route prerendered to a file has no code present at all, so the file host returns whatever it is configured to return — often 200, unless that file is registered as its error document. The result is a soft 404: indexed by crawlers, stored by shared caches, and invisible to status-based monitoring.

go deeper

for a junior

Remember that the status code and the visible page are set by different parts of the response, so a page that says not-found does not by itself make the response a 404.

for a middle

Explain the ordering: headers commit first, so an absence discovered by the data step must be declared before the body starts, and say what each rendering mode allows.

for a senior

Demonstrate the diagnosis. Read status lines rather than pages, know why soft 404s get indexed and cached, and place the fallible data work ahead of the first flush on streamed routes.

for a principal

Treat response status as an operational contract shared by crawlers, caches, clients and dashboards, and decide which routes may stream at all given that streaming trades accurate failure reporting for early first bytes.

## Two different things called "not found" A request can fail to produce a page in two structurally different ways, and only one of them is the router's business. 1. **The router misses.** No route pattern in the tree matches the path. Nothing has rendered, nothing has been sent, and the framework is free to answer 404 with its not-found page. 2. **The route matches but the data is missing.** A dynamic segment matched — the path is shaped like a product URL — and the route's data step then discovers there is no such product. The router did its job; the *content* is absent. The second case is where the status and the page drift apart, because by the time the absence is known, rendering has already begun and, depending on the rendering mode, the response may already be on the wire. ## Status is a header; the page is a body An HTTP response sends its status line and headers first, then the body. Once those bytes are flushed the status is settled: nothing rendered afterwards can change it. So the rule is simple and mechanical — **the not-found outcome has to be declared before the response is committed**, or the page you render says "not found" while the status says 200. That mismatch is what people mean by a *soft 404*. Meta-frameworks differ in how the declaration is expressed: some have the data step abandon rendering by signalling a not-found outcome that the router catches and turns into a status plus the nearest not-found page; others expect the route to return a status alongside its data. The shape differs, the constraint does not. ## What each rendering mode can actually produce | Rendering mode | Present when the request arrives | Status it can set | |---|---|---| | Prerendered to files at build time | no code of yours; a file host or CDN answers | whatever the host is configured to return for that file — commonly 200, unless that file is registered as the host's error document | | Rendered per request on a server or function | your code, before anything is sent | any status, chosen from the request and its data | | Streamed | your code, but the shell was flushed early | fixed at flush; a later absence can only be expressed inside the body | | Regenerated prerender | the existing copy answers; refresh happens behind it | the stored copy's status, until a successful rebuild replaces it | This table is the real answer to "why do they disagree". A prerendered not-found page is just a file; a file host has no idea the markup means absence. A streamed route that discovers a missing record after the shell has gone out has already promised 200. A regenerated route keeps answering with the last good copy while the refresh fails in the background. ## Why a soft 404 is worth fixing - **Crawlers** index the page. A site can accumulate thousands of indexed "not found" URLs that all report success. - **Monitoring and error budgets** under-count: dashboards built on status codes see a healthy site. - **Shared caches** store it under the normal rules for a successful response, so the wrong page can be served to other users for the whole freshness window. - **Programmatic clients** — anything calling the route and branching on the response status — take the 200 at face value and try to read a record that is not there. - **Redirect-to-home as a substitute** is worse than either: it answers 3xx and then 200, so absence is never reported at all and the visitor loses the URL that would have told them what happened. ## Fixing it in practice 1. Decide absence in the **data step**, not in the component. A component that renders a friendly "we could not find that" is rendering a body; it is not setting a status. 2. Declare it **before streaming starts**. If a route streams, the portion of the work that can conclude "this does not exist" belongs before the first flush, even when the rest of the page streams. 3. For a **prerendered** deployment, accept that the artifact cannot carry a status and configure the host to serve that file as its error document, so the host supplies the 404. 4. **Verify with the protocol, not the browser.** Fetch the URL and read the status line. A browser shows you the page and hides the number, which is exactly why this defect survives review. 5. Treat the **error** case the same way: the page a route falls back to when rendering throws must carry a 5xx unless the failure genuinely is the client's fault, and that too is only possible before commit. The one-sentence version to say out loud: *the page is what rendered, the status is what was promised before rendering finished, and the two agree only if the absence is known early enough — which the rendering mode decides.*

  • Why is redirecting a missing record to the home page a poor substitute for a 404?
    It answers 3xx and then 200, so absence is never reported to anything: crawlers keep the dead URL alive, monitoring sees success, and the visitor loses the one page that would have explained what happened. A redirect is a statement that content moved, not that it is gone.
  • How would you catch soft 404s that already exist in a codebase?
    Ask for status lines rather than pages: crawl the site or replay known-bad URLs and assert the status, and add the same assertion as a test for each route with a dynamic segment. Search-console style reports and log aggregation on status per path both surface the pattern quickly, since the giveaway is a 200 on a URL nobody links to.
  • Does the same constraint apply to the error page a route falls back to?
    Yes. The fallback error page is a body like any other, so it can only carry a 5xx if the failure is known before the response commits. A failure arriving mid-stream leaves you with an error UI under a 200, which is why routes that must report failures accurately keep the fallible work ahead of the first flush.

saying these in an interview costs you the question

  • Thinks rendering a not-found component sets the status automatically
  • Says any route with no data always answers 404
  • Believes a prerendered HTML file can choose its own status
  • Checks the outcome in a browser instead of reading the status line
  • Redirects missing records to the home page and calls it handled
  • Assumes a streamed route can still switch to 500 mid-body