Your 404 Page Looks Right — But Does It Actually Return 404?
A SPA can render a perfect not-found screen while still sending HTTP 200. Learn how to audit soft 404s, client routing, status codes, monitoring, and indexing.
You mistype a URL. The site displays a polished “Page not found” screen, offers search, and links back to the homepage. Visually, everything looks correct.
Then you open the network panel and inspect the first document request: 200 OK.
That is not a cosmetic detail. HTTP 200 says the request succeeded. HTTP 404 says the origin server did not find a current representation for that target resource. When the content and the status code tell different stories, you have a soft 404.
This is particularly easy to create with single-page applications, static-hosting fallbacks, and broad client-side routing.
The most useful audit question is therefore:
What does the server return for a URL that does not exist, before JavaScript has a chance to repair the experience?
Why a polished 404 screen is not enough
People mainly see the rendered result. Crawlers, monitoring systems, proxies, and other technical clients also see the HTTP protocol.
RFC 9110 defines 200 OK as indicating that a request succeeded. 404 Not Found indicates that the origin server did not find a current representation for the target resource, or is not willing to disclose that one exists.
If /products/definitely-not-real returns 200 and JavaScript later renders a “Not found” screen, the application has shown an error to the person while reporting success at the protocol layer.
That mismatch is the core problem.
How SPAs fall into this trap
Many client applications need the same HTML entry point for every valid app route. Hosting or server configuration therefore often works roughly like this:
- if a real file exists, serve it;
- otherwise serve
index.html; - let the client router decide which screen to display.
That is convenient for valid routes. For invalid routes, it can mean every arbitrary URL initially returns 200.
The router may later discover that nothing matches. It cannot retroactively change the HTTP status of the document response that has already been delivered.
The same pattern appears on dynamic detail pages. The app shell loads successfully with 200, an API request later returns 404, and only then does the interface display “Record not found.” That can be perfectly reasonable for an internal application. For public, indexable URLs, however, teams should deliberately decide what the URL is meant to signal externally.
What Google means by a soft 404
Google Search Central describes a soft 404 as a URL that tells the user the page does not exist while returning 200 (success). Google may exclude these pages from Search and report them as soft 404s in Search Console.
Google's JavaScript SEO guidance also recommends meaningful HTTP status codes. For client-side SPAs where returning a real status on the original route is difficult, the documentation describes two fallback patterns:
- use a JavaScript redirect to a URL that returns an actual 404 from the server;
- add a
noindexrobots meta tag to error pages with JavaScript.
Those are useful safeguards. They do not remove the architectural question: Can your public routing layer represent real failures as real failures?
Four questions for a useful audit
1. What status does the first document request return?
Do not stop at the API response. Do not judge only the rendered page.
Open a URL that you know cannot exist in a fresh tab and inspect the first HTML request. A visual 404 paired with 200 OK is the classic soft-404 pattern.
Test deeper paths too:
/services/not-real/blog/2021/removed-article/en/products/wrong-slug- a random UUID or obviously invented record identifier
This helps reveal whether the issue affects one template or the entire fallback strategy.
2. What happens without existing client state?
SPA routing failures often appear only on direct entry.
An internal click may work because the router is already loaded and data is in memory. The same deep link can behave differently after a reload, in a new browser window, or when requested by a crawler.
At minimum, compare:
- direct entry;
- reload on the route;
- navigation inside the application;
- the URL without JavaScript where meaningful;
- rendered DOM versus the initial server response.
3. Are different failure states collapsed into one 200 screen?
Not every failure means the same thing.
An unknown public URL, a missing record, a protected route, a temporary backend outage, and deliberately removed content are different states. They should not all become “200 plus some error message” by accident.
This does not mean every internal application needs a perfect document-level status for every condition. It means the team should know which layer owns each failure and what users, crawlers, APIs, and monitoring systems actually receive.
4. Would your monitoring notice the failure?
If an external monitor only asks whether a URL returns 2xx, a soft 404 can look healthy.
A meaningful website check therefore combines status codes with expected content and behaviour. For example:
- Should a known landing page return 200 and contain a specific marker?
- Should an invented URL return 404?
- Should removed content return 404 or, where intentionally chosen, 410?
- Should a protected resource behave differently under your security model?
- Does the public error page itself still load navigation, assets, and support paths reliably?
A status code is a signal. An audit checks whether the signal matches reality.
A pragmatic implementation path
Server-rendered or hybrid: decide errors before sending the response
Where routing and data lookup can happen on the server, “not found” should ideally be decided while the HTTP response can still be controlled.
The visitor can still receive a well-designed error page, but the response also carries the appropriate status.
For public content, product, profile, or editorial pages, this is usually the cleanest model.
Pure client SPA: constrain the fallback deliberately
A catch-all may be unavoidable on a purely static SPA deployment. In that case, review whether:
- public indexable areas can be handled separately on the server;
- known error states can move to a real server-side 404 URL;
- error pages reliably receive
noindexwhen they should not be indexed; - sitemaps, canonicals, and internal links generate only valid URLs;
- hosting rules avoid converting every missing asset or document URL into the app shell.
That last point is easy to miss. A missing JavaScript bundle or JSON resource should not unexpectedly receive HTML with status 200 simply because a global SPA rewrite is too broad.
Do not confuse API status with document status
An application can return 200 for its document and later receive a correct 404 from /api/products/123. That is not automatically wrong.
The audit question is what the public URL represents.
If /products/123 is supposed to be a standalone, indexable product page, a missing product deserves different treatment from a private app view behind authentication. Architecture, SEO requirements, and operating model need to agree.
Regression tests that are cheap and effective
Soft 404 checks belong in smoke tests or end-to-end tests.
A small set often catches most problems:
- known valid URL → expect 200;
- clearly invented URL → expect 404 or a deliberately documented alternative;
- deleted record → expect the defined error response;
- invalid slug on a dynamic route → no silent 200;
- missing asset → no HTML fallback with 200;
- error page → useful navigation and no contradictory canonical;
- if
noindexis the SPA fallback → confirm it exists after rendering.
Run these checks again after relaunches, hosting migrations, router upgrades, and CDN rule changes. Rewrite behaviour can change even when nobody has touched the visible 404 design.
What Website-Pflichtencheck would inspect
A technical website review should not stop at whether the error page looks good.
We can inspect:
- real HTTP status codes for valid and invalid URLs;
- SPA, CDN, and hosting fallbacks;
- dynamic routes and removed content;
- initial HTML responses versus later API failures;
noindex, canonical, and sitemap consistency;- missing assets and rewrite side effects;
- monitoring signals;
- mobile and client-side navigation;
- regressions after deployments.
The goal is not an academically perfect status-code matrix. The goal is for browsers, search engines, and operations tooling to receive the same basic truth about a URL.
If your 404 page only says “404” in its text, the most important part of the test is still unfinished.