Principles
An error page is not a visual notice but an interface that correctly conveys the facts at the HTTP level while guiding humans, search engines, API clients, and AI agents to their next action.
The top priority is the status code. CDNs, load balancers, SDKs, browsers, and crawlers primarily determine meaning from the response's status line. The text "404" or fields in the body are merely supplementary information.
Do not create soft 404s that return a normal page with 200 OK for a nonexistent URL. At the HTTP level, this declares that "the resource exists," causing crawlers, monitoring, SDKs, and caches to operate on a false premise.
# すべて不適切
HTTP/1.1 200 OK ← 本文に <h1>404 Not Found</h1> があっても無意味
HTTP/1.1 200 OK ← 本文が { "status": 404 } でも無意味
HTTP/1.1 302 Found ← 代替がないのにトップへ飛ばす
Location: /
Do not redirect to the top page or search results when there is no alternative page. It may look helpful to users, but it misleads them into thinking the requested resource has moved. If the alternative is truly equivalent, use 301 or 308; otherwise, provide navigation in the body of a 404 or 410.
Choosing the code
- 404 Not Found — The URL is wrong, not routed, or existence cannot be confirmed. It means there is no representation at this point in time, and does not assert permanent deletion. It can also be used when you do not want to reveal existence.
- 410 Gone — It existed in the past and was intentionally and permanently discontinued. Choose this only when it is certain that there are no plans to restore it and no alternative destination.
- 301 Moved Permanently / 308 Permanent Redirect — Only when there is definitely an equivalent destination.
- 503 Service Unavailable — Maintenance, overload, or dependency failure. Temporarily unavailable.
- 401 Unauthorized — Authentication credentials are required or insufficient.
- 403 Forbidden — Access is not allowed even when authenticated.
Whether to hide unauthorized resources with 404 or explicitly indicate them with 403 or 401 should be made consistent across the board as a security policy. Do not let it vary from page to page.
Implementation order
- At the routing layer, capture anything other than known pages, APIs, and static files.
- Make only the mapping table from old URLs to equivalent new URLs return 301 or 308.
- Make only public URLs whose discontinuation is confirmed return 410.
- Make all other unknown URLs return 404.
- Serve HTML, Markdown, or application/problem+json bodies according to Accept.
- Add Vary: Accept to prevent cache confusion between representations.
- Collect 404 logs, and use referrers, frequently occurring URLs, and bot access to inform link fixes and redirects.
HTML representation
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>ページが見つかりません | Example</title>
<meta name="robots" content="noindex">
</head>
<body>
<main>
<h1>ページが見つかりません</h1>
<p>URL が間違っているか、ページが移動・削除された可能性があります。</p>
<nav aria-label="次の操作">
<a href="/">ホームへ戻る</a>
<a href="/search">サイト内を検索</a>
<a href="/docs/">ドキュメント</a>
<a href="/sitemap.xml">サイトマップ</a>
</nav>
</main>
</body>
</html>
- State clearly that it was not found. "An error occurred" alone does not reveal the cause.
- Place links to the home page, site search, main categories, documentation, and contact information.
- If you display the requested URL on the screen, do not show the query string, tokens, or email addresses as-is.
- Stay on brand while not making it confusable with regular content pages.
- noindex is not mandatory. The main purpose is to return a correct 4xx. You may add it if you want to avoid the 404 template itself appearing in search results.
- Do not completely block crawling of nonexistent URL groups in robots.txt. If they cannot be accessed, the 404 or 410 status cannot be verified. If you want to remove unnecessary pages, first ensure correct 404 and 410 responses, removal from the sitemap, and fixing internal links.
API representation
Use RFC 9457 Problem Details.
HTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8
Cache-Control: no-store
{
"type": "https://example.com/problems/resource-not-found",
"title": "Resource not found",
"status": 404,
"detail": "No resource matches this URL. See https://example.com/docs/.",
"instance": "urn:request:01JABC..."
}
- The status in the body is advisory. It must always be the same value as the actual HTTP status line.
- The detail should not be debug information; focus on an explanation that helps the client fix the problem.
- Do not let clients parse strings to create control flow. Determine based on HTTP status and a stable type URI.
- If possible, place human-readable documentation at the type URI.
- Make it so clients can ignore unknown extension fields.
Markdown representation
For agents. Markdown is not a substitute for 404; it is one body representation of a 404. The status always remains 404.
HTTP/1.1 404 Not Found
Content-Type: text/markdown; charset=utf-8
Vary: Accept
Cache-Control: no-store
# Not found
This URL does not exist.
- [Documentation index](/docs/)
- [Sitemap](/sitemap.xml)
- [LLM guidance](/llms.txt)
Do not return only Markdown to browsers expecting HTML. Differentiate via Accept. On developer sites, API sites, and product documentation, include a starting point for exploration so that failed requests do not become dead ends.
HEAD
HEAD /does-not-exist returns the same status as GET, only without a body. HEAD responses do not include a body.
Caching
404 and 410 can be cached. This is effective for sites with many accesses to nonexistent URLs, but it becomes an incident if resources that may reappear immediately after publication or within a short period are retained for a long time.
- URLs that are completely abolished and will not reappear — 410 and short-to-medium caching.
- Typos, random URLs, URLs that may appear dynamically — 404 with no-store or a short max-age.
- URLs that intentionally return 404 for authorization — private, no-store to avoid information leakage via shared caches.
- If there is a CDN, check the 404 behavior of the browser, CDN, and application separately.
Do not leak
Do not include the following in error bodies.
- Stack traces, exception messages, source map URLs.
- Framework names and versions, internal IPs, hostnames.
- Repository paths, DB table names, cloud bucket names.
- User IDs, email addresses, session information.
- Response differences that allow determining "this private resource exists but you do not have permission."
Problem Details is not a mechanism for exposing implementation debugging information.
Verification
# ステータスだけ見る
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/no-such-path
# リダイレクトを追わず最初の応答を見る
curl -sS -D - -o /dev/null https://example.com/no-such-path
# HEAD でも同じか
curl -sSI https://example.com/no-such-path
# API 表現
curl -sS -i -H 'Accept: application/problem+json' https://example.com/api/no-such-path
# Markdown 表現
curl -sS -i -H 'Accept: text/markdown' https://example.com/no-such-path
Check.
- Whether the status line is really 404, or the intended 410.
- Whether there is any unintended 30x.
- Whether the Content-Type matches the actual body. If JSON, is it application/problem+json?
- If status is included in the body, does it match the HTTP status?
- Does the HTML have a path for users to recover? Are the Markdown links valid?
- Is there any secret or internal information in the body?
- Does HEAD return the same failure status without a body?
- Does the same result occur on the production domain via a CDN or proxy?