ByteScope

500

HTTP 500 Internal Server Error 怎麼修

5xx 伺服器錯誤IETF 標準RFC 9110

應用程式自己跑起來了,撞到一個它沒預期的東西,然後放棄——所以跟 502、504 不一樣,某個地方一定有一份 stack trace,而整件事的工作就是找出它在哪一份紀錄裡。

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

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

用 curl 重現 500

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

curl
curl -sS -o /dev/null -D - -w '\n%{http_code} in %{time_total}s\n' -H 'content-type: application/json' --data '{"quantity":-1}' https://example.com/api/orders

`-sS` 會把進度列消掉但留下錯誤訊息,`-o /dev/null` 把 body 丟掉,`-D -` 把回應標頭印到標準輸出,`--data` 送出 payload 並讓這個請求變成 POST,`-w` 則印出狀態碼和實際耗時。先讀標頭那一段,因為它回答了狀態行答不出的那個唯一問題:這是哪一層寫的。`server:` 寫著你的應用程式框架,或是你的技術堆疊會設的任何 `x-request-id`,代表請求有到你的程式碼,而例外就在你紀錄裡那個 id 底下。`server: nginx` 或 `server: cloudflare` 而且完全沒有應用程式的標頭,代表你看到的是應用程式前面那一層的錯誤頁,那三位數字就值得重讀一次——一個拿不到可用答案的中介裝置報的是 502,不是 500。耗時是第二個線索:應用層級的 500 通常回得跟成功一樣快,因為例外是被丟出來的、不是等出來的,所以一個花三十秒的 500 是你程式碼裡的逾時,指向那個卡住的依賴,不是指向丟例外的那一行。把 payload 換成一份你確定合法的,確認這個端點本身是好的;然後把失敗那份留著——那就是重現方式,也是後端從紀錄裡救不回來的東西。

500 實際上在講什麼

RFC 9110 §15.6.1 把 500 定義成:伺服器遇到一個非預期的狀況,導致它無法完成這個請求;而它所屬的 5xx 類別,§15.6 的引言寫的是伺服器沒有能力執行這個請求。這個定義是整份規格裡刻意最不具體的一個,而那正是有用的地方:500 是一句自承,不是一份診斷,所以這個代碼完全沒有攜帶「什麼壞了」的資訊,只攜帶「是誰發現的」。從這裡可以推出三件事。第一,你的應用程式產生的 500 是一個完全合法的 HTTP 回應,所以前面的代理會原封不動轉出去,不會拿自己的頁面替換掉——這正是為什麼 RFC 9110 §15.6.3 把 502 留給「中介裝置收到不合法回應」的情況,也是為什麼 500 是「你的程式碼有跑到」的證據,而 502 通常代表它根本沒跑。第二,500 不在 RFC 9110 §15.1 那份可依啟發式規則快取的狀態碼清單裡——那份清單只有 200、203、204、206、300、301、308、404、405、410、414 和 501,再無其他——所以沒有中介裝置會自作主張把它存起來,而一個持續出現的 500 是每個請求都被重新產生一次,不是從快取放出來的。第三,500 完全沒有在講「這個請求有沒有產生效果」:規格沒有給用戶端任何方法去知道伺服器在撞上那個非預期狀況之前走到了哪一步,這就是為什麼付款或下單上的 500 不能自動當成可以安全重試。伺服器也會從自己的機制裡吐出 500,不只是應用程式碼——Apache 的 CGI 文件寫著,一個錯誤紀錄寫著「Premature end of script headers」的「Internal Server Error」,代表腳本在它的 HTTP 標頭之前就先印了東西,或者 suexec 的權限檢查拒絕了它;而 nginx 則會為自己內部的故障回 500,例如 rewrite 或內部轉址繞成迴圈。這兩種情況下,三位數字都一樣,而那一行紀錄就是全部的內容。

500 最常被搞混的幾個代碼

容易搞混的怎麼分
502500 是應用程式承認自己失敗了,502 是中介裝置在報告應用程式從來沒交出一個可用的答案。RFC 9110 §15.6.3 把 502 定義成「從內部伺服器收到不合法的回應」,而應用程式自己的 500 是一個合法的回應,所以代理會直接原封不動轉出去。實務上是這樣:500 代表有一份 stack trace 可以找,而 502 常常代表工作行程在任何處理器跑起來之前就死了,應用程式紀錄是空的。
503503 是一個決定,500 是一場意外。RFC 9110 §15.6.4 把 503 定義成伺服器因為過載或排定的維護而暫時無法處理這個請求,並且允許它帶 `Retry-After` 說明要多久。500 裡沒有任何東西在暗示「等一下」,因為它身上沒有任何一件事被期待會過去。如果你的服務是靠丟例外來降載,那它是把一次計畫中的拒絕報告成一次當機,而所有以 503 為判斷依據的用戶端退避邏輯全都會漏掉它。
400RFC 9110 §15.5.1 把 400 定義成:伺服器因為察覺到某種用戶端錯誤而無法處理這個請求。那才是你的端點在拒絕輸入時誠實的代碼。一個在驗證輸入時冒出來的 500,是錯誤處理的 bug、不是伺服器的 bug:這個請求是答得出來的,答案是「不行」,而 5xx 卻在叫用戶端去重試一件永遠不會成功的事。
Cloudflare 520透過 Cloudflare 的話,這兩個很好分,而且這個差別值得知道。Cloudflare 的文件把 520 定義成來源機回了一個空的、未知的或非預期的回應——包括標頭超過它 128 KB 上限的情況——所以 520 代表你來源機的答案不是可用的 HTTP。而透過 Cloudflare 抵達的 500 剛好相反:那是你的應用程式刻意產生的一個格式良好的回應,Cloudflare 只是原封不動轉過來。
501RFC 9110 §15.6.2 把 501 定義成伺服器不支援完成這個請求所需的功能,並指名它是伺服器不認得該方法時該給的答案。跟 500 不一樣,它是在陳述伺服器的能力,不是在陳述某一個請求出了錯;而且它是 RFC 9110 §15.1 列為可依啟發式規則快取的少數錯誤代碼之一——所以一個誤發的 501 可能被快取存起來、在原因消失之後還繼續重播,500 則不會。
原因片語
Internal Server Error
類別
5xx 伺服器錯誤
定義出處
RFC 9110
標準
IETF 標準

站上可以搭配的工具

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

相關狀態碼

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

常見問題

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

去讀回應標頭,不要讀那個網頁。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,會把值班的人送去堆疊裡錯的那一半。

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