ByteScope

404

HTTP 404 Not Found 怎麼修

4xx 用戶端錯誤IETF 標準RFC 9110

伺服器看懂了你的請求,只是那條路徑上沒有東西可以給你。在現在的架構裡,這比較常是路由或部署漏掉了一步,而不是誰貼了一條死連結。

為什麼會看到 404,以及該怎麼處理

同樣三個數字,你站在請求的哪一邊,它就是不同的問題。直接讀你自己那一塊。

用 curl 重現 404

對著出錯的那個網址跑一次。它只印狀態碼、不印錯誤頁,所以你看到的是伺服器真正說了什麼,而不是瀏覽器畫出來的東西。

curl
curl -sS -o /dev/null -L -w '%{http_code} %{num_redirects} %{url_effective}\n' https://example.com/no-such-page

`-sS` 會把進度列拿掉但保留錯誤訊息,`-o /dev/null` 把錯誤頁的內容丟掉,`-L` 會跟著 `Location` 標頭走,`-w` 則印出最後的狀態碼、跟了幾次轉址,以及回出這個狀態的那個 URL。最後那一欄才是這行指令的重點:curl 自己的文件寫著,`%{url_effective}` 要在你叫它跟著轉址走的時候才最有意義,而一連串轉址走到底才出現的 404,跟你自己打的網址就 404 是兩種不同的 bug——它代表有一條改寫規則把你送去了一個不存在的地方。你以為是直達的網址卻出現 `%{num_redirects}` 大於零,那就是線索。至於 404 是邊緣節點還是來源機做的,同一個請求跑兩次就知道:一次照原樣,一次加上 `--resolve example.com:443:203.0.113.10`,它會照樣送出原本的主機名稱和 SNI,但連到你指定的位址。兩次答案不一樣,就代表邊緣節點正在發一份來源機沒有的東西——以這個狀態碼來說,通常是一份被快取起來的 404。

404 實際上在講什麼

RFC 9110 §15.5.5 把 404 定義成:來源伺服器找不到目標資源目前的表示,或者不願意透露它存在。這兩半都會影響你怎麼查。前半的意思是,404 只在講「現在這個 URL」,它完全沒有說這個資源以前存不存在、以後會不會出現;同一節還寫了,如果伺服器知道這個狀況是永久的,410 (Gone) 是更好的答案。後半就是 404 和 403 會重疊的原因:§15.5.4 明白允許一台不想承認某個被禁資源存在的來源伺服器,改回 404。所以你很確定存在的 URL 卻回 404,有可能是一個換了裝的存取控制決定。還有一點會在事故當下嚇到人:404 是 RFC 9110 §15.1 列為可依啟發式規則快取的少數狀態碼之一,就算你沒有寫任何 `Cache-Control`,中間的代理或 CDN 也可以把它存起來。於是一個在壞掉的部署期間回 404 的 URL,即使部署修好了還是會繼續回 404,直到那份存起來的回應過期、或有人去清掉為止。最後,404 對方法沒有任何意見:對一條只收 GET 的路徑送 POST,那是 405 不是 404,在那裡回 404 的框架,等於選擇不告訴你錯的是哪一半。

404 最常被搞混的幾個代碼

容易搞混的怎麼分
403403 是「我看懂了,但我拒絕」,404 是「這裡沒有東西」。RFC 9110 §15.5.4 是刻意把兩者弄模糊的:它明白允許一台不想承認某個被禁資源存在的來源伺服器改回 404。所以當你很確定存在的網址回了 404,先帶上認證資訊再試一次,然後才去找路由的 bug——物件儲存的儲存桶和私有版本庫就是這樣做的。
410410 (Gone) 是伺服器知道這個資源已經永久結束時該給的答案,RFC 9110 說在那種情況下 410 比 404 更合適。差別是給機器看的信號,不是給人看的:404 會招來重試,410 則是告訴索引器不用再問了。如果你是刻意要下架內容,410 比較誠實,從搜尋結果消失得也比較快。
405405 (Method Not Allowed) 的意思是路徑在,只是不收你用的那個方法,而 RFC 9110 要求回應要帶一個 `Allow` 標頭,列出真正可用的方法。對一條只收 GET 的路由送 POST 卻回 404 的框架,規格上是允許的,但它把答案裡有用的那一半藏起來了——所以在懷疑路徑之前,先確認方法。
soft 404soft 404 是那種內容裡寫著「找不到」、狀態行卻是 200 的頁面。HTTP 沒有禁止這樣做,但搜尋引擎會把錯誤頁當成內容收進索引,而腳本看到成功的狀態碼,就會把那段道歉文字當成資料去解析。如果一頁你明知不存在、用 curl 打卻沒有回 404,那個不一致本身就是 bug。
nginxnginx 會回 404 的地方不只「檔案不在」一種。以 `=404` 結尾的 `try_files` 是刻意回的;`Host` 對不上任何 `server_name` 的請求會落到預設伺服器,而那台通常是一個根目錄空空的樣板;掉了結尾斜線的 `alias` 則會組出一條少一層目錄的路徑。錯誤紀錄裡會寫它實際去試的那條路徑,那是分辨這三種最快的方法。
原因片語
Not Found
類別
4xx 用戶端錯誤
定義出處
RFC 9110
標準
IETF 標準

站上可以搭配的工具

這些全都在你的瀏覽器裡跑,不會上傳任何東西。

相關狀態碼

查 404 的時候,通常也會順手看一下這幾個。

常見問題

怎麼知道這個狀態碼是哪一台伺服器回的?

去讀回應標頭,不要讀那個網頁。curl -sS -o /dev/null -D - https://example.com/path 會只印標頭、不印內容,而 serverviacf-ray 三個加起來,就能指出是哪一層回答的。有 cf-ray 的值代表這個回應是 Cloudflare 處理的,那個值也是他們客服會跟你要的編號。想把邊緣節點整個排除掉,就加上 --resolve example.com:443:203.0.113.10 再打一次:它會照樣送出原本的主機名稱和 SNI,但直接連到你指定的來源位址——答案變了,就代表邊緣節點和來源機講的不是同一件事。

數字後面那句原因片語(像 Not Found)重要嗎?

不重要,而且絕對不要拿它來做判斷。RFC 9112 要求用戶端忽略原因片語,伺服器可以隨意改它,而 HTTP/2 和 HTTP/3 根本沒有這個欄位——所以在 HTTP/1.1 上是 404 Not Found,到了 HTTP/2 就只剩一個 404。這個網站會寫出登記在案的片語,是因為那是大家會拿去搜尋、也是會出現在 HTTP/1.1 紀錄裡的字串,不是因為有任何軟體依賴它。

這個代碼失敗的請求該不該重試?

5xx 和 429 可以重試,4xx 不要——下一次送過去也不會有任何不同。如果回應帶了 Retry-After 就照它做,RFC 9110 定義它就是為了這件事,值可以是秒數也可以是一個時間點。沒有的話,就用帶抖動的指數退避加上一個硬上限,而且只對冪等的方法這樣做:一個被重試的 POST 可能會刷兩次卡。對一台已經在失敗的伺服器丟出重試風暴,是短暫事故變成長時間事故最常見的那條路。

我可以讓自己的伺服器改回別的狀態碼嗎?

可以,唯一要守的規則是那個代碼必須是真的。所有會自動讀你回應的東西——搜尋引擎、監控、快取、用戶端的重試邏輯——都只看那個數字,從來不看那一頁寫了什麼。用 200 回一頁錯誤訊息,會把故障從你自己的警報裡藏起來;用 200 回一頁不存在的內容,會讓錯誤被當成內容收進索引;而對一個只是格式不對的請求回 500,會把值班的人送去堆疊裡錯的那一半。

404 還是卡住嗎?看完整的狀態碼對照表,或是回到上面,讀為你這一端寫的那一塊。