502
應用程式前面那台代理替它回了話,因為應用程式自己的回應沒來、中途斷掉,或根本不是合法的 HTTP。
同樣三個數字,你站在請求的哪一邊,它就是不同的問題。直接讀你自己那一塊。
為什麼會看到它
你的請求不是在送出的階段失敗,是在解析的階段失敗。代理的 502 頁面是 HTML,所以 `res.json()` 會丟出 `Unexpected token '<'`,而堆疊追蹤指的是你的解析器,不是那台壞掉的伺服器。它通常只打中某一個端點而不是整個網站——比較慢的那個、後面掛著冷啟動容器的那個、剛剛有人部署過的那個——而且在來源機重啟的期間,它會在兩次載入之間忽有忽無。
該怎麼做
不要在看狀態碼之前就相信內容。解析前先看 `res.ok`,再讀 `content-type`:502 的內容是 `text/html`,你的 API 真正的錯誤不是。接著在網路面板裡把失敗的那個請求打開,讀回應標頭——`server`、`via`、`cf-ray` 會指名是誰寫的,而這就是整個問題所在。只重試冪等的請求,而且要加退避和上限,因為對一台正在重啟的來源機丟出重試風暴,會把十秒的小抖動變成一次事故。還有,不要把整個下午花在 CORS 上:錯誤頁不會帶 `access-control-allow-origin`,所以瀏覽器會在真正的失敗上面再疊一層 CORS 失敗,那則 CORS 訊息是症狀不是原因。
為什麼會看到它
你的代理沒能從應用程式那裡拿到一個可用的答案,而在 nginx 上,錯誤紀錄那一行就是診斷結果。值得記起來的只有幾種。`connect() failed (111: Connection refused) while connecting to upstream` 代表 `proxy_pass` 那個位址上沒有人在聽。`upstream prematurely closed connection while reading response header from upstream` 代表工作行程在請求進行到一半死了——被 OOM 砍掉、同步工作行程裡有未處理的例外,或請求活得比應用伺服器自己的逾時還久。`upstream sent too big header while reading response header from upstream` 代表回應標頭超過了 `proxy_buffer_size`,而那正是一個過肥的 `Set-Cookie` 會做的事。
該怎麼做
先讀代理的錯誤紀錄,其他都放後面:它會同時寫出原因和 upstream 的位址,這兩樣東西比你在應用程式那邊加多少紀錄都更快給出答案。接著確認兩端講的是同一件事:應用程式實際綁在哪個位址和連接埠,那是不是 `proxy_pass` 裡寫的那一個?綁在 `127.0.0.1:3000` 的應用程式,對一台在另一個容器或另一個網路命名空間裡的代理來說是連不到的,而那跟行程掛掉會呈現成同一種「被拒絕」。如果紀錄指的是連線太早斷,就去看應用程式自己的請求逾時和記憶體上限;如果它說標頭太大,就把 `proxy_buffer_size` 調高,然後去查標頭為什麼會長成那樣。在 Cloudflare 上,看那頁錯誤頁有沒有 Cloudflare 的品牌樣式:Cloudflare 的文件寫著,有品牌樣式的那頁是 Cloudflare 產生的,沒有的那頁是你的來源機回的、Cloudflare 只是轉過來而已。
為什麼會看到它
你這邊什麼都沒壞,而且你在自己電腦上做什麼都不會改變它。502 的意思是那個網站自己的機器之間講不上話,所以你現在讀到的這則訊息,是在網站連不上的期間,由它前面那一層寫出來的。它通常只持續幾秒到幾分鐘,多半發生在部署或當機的時候,而且常常只影響網站的一部分——首頁開得好好的,結帳卻掛掉。
該怎麼做
等一分鐘再重新整理一次。清 cookie、換瀏覽器、清 DNS、重開路由器,在這裡全都是白費力氣,因為壞掉的是連線的另一端。如果它持續超過幾分鐘就值得回報,而回報時真正有用的是準確的時間,以及錯誤頁上印的 `cf-ray` 值或請求 ID——那是網站維運的人真的查得到的字串。如果那是你有付費的服務,看它的狀態頁是最快知道「他們是不是已經發現了」的方法。
對著出錯的那個網址跑一次。它只印狀態碼、不印錯誤頁,所以你看到的是伺服器真正說了什麼,而不是瀏覽器畫出來的東西。
curl -sS -o /dev/null -D - -w '\n%{http_code} in %{time_total}s\n' https://example.com/api/`-sS` 會把進度列消掉但留下錯誤訊息,`-o /dev/null` 把那頁 HTML 錯誤頁丟掉,`-D -` 把回應標頭印到標準輸出,`-w` 則在傳輸結束後印出狀態碼和實際耗時。這份輸出裡有三樣東西決定你下一步看哪裡。狀態行確認它真的是 502,而不是應用程式自己回的 500。標頭那一段指名作者——`server: nginx` 而且沒有 `via`,那是你自己的代理;`server: cloudflare` 加上一個 `cf-ray`,那是 Cloudflare,而那個 `cf-ray` 的值就是他們客服會跟你要的編號。至於耗時,那是跟 504 分家的關鍵:被拒絕或壞掉的 upstream 在毫秒內就回,逾時的那種則是照著計時器回——nginx 預設的 `proxy_read_timeout` 是 60 秒,Cloudflare 則是撐到 125 秒才丟出自己的 524。要證明問題在代理而不在來源機,同一行加上 `--resolve example.com:443:203.0.113.10`,它會照樣送原本的主機名稱和 SNI,但直接連到你指定的來源位址。
RFC 9110 §15.6.3 把 502 定義成:一台以閘道或代理身分運作的伺服器,在試著完成請求時,從它連進去的內部伺服器收到了不合法的回應。從這個寫法可以推出兩件事,而且兩件都會改變你該看哪裡。第一,寫出這個狀態碼的是中間那一層——nginx、HAProxy、Envoy、負載平衡器、Cloudflare——不是你的應用程式,而且你的應用程式很可能根本沒被執行到。第二,「不合法的回應」比「錯誤的回應」涵蓋得寬得多:TCP 連線被拒、回應標頭還沒收完連線就斷了、代理看不懂的狀態行、大到超過代理緩衝區的標頭,全都算;但應用程式成功回了自己的 500 不算,因為那是一個合法的回應,代理會原封不動轉出去。502 也不是逾時:RFC 9110 §15.6.5 把「沒有及時收到回應」留給了 504。這個區別附帶一支碼表——502 通常在毫秒內就回來,504 一定是在某個計時器剛好走完的那一刻回來。最後,502 不在 RFC 9110 §15.1 那份可依啟發式規則快取的清單裡,所以中間裝置不會自作主張把它存起來;而 nginx 預設會在 `error` 和 `timeout` 時換下一台 upstream,但除非你在 `proxy_next_upstream` 明寫 `non_idempotent`,否則它不會替非冪等的請求這麼做。
| 容易搞混的 | 怎麼分 |
|---|---|
| 504 | 兩者都是代理在報告 upstream 的狀況,而界線 RFC 9110 已經幫你畫好了:502 是不合法的回應,504 是沒有及時到的回應。讀碼表比讀字面有用。502 通常遠不到一秒就回來,因為 upstream 拒絕了連線或把它切了;504 會在一個整數上回來——nginx 預設的 `proxy_read_timeout` 是 60 秒、Cloudflare 是 125 秒——因為有人坐在那裡把計時器等完了。 |
| 500 | 500 是應用程式承認自己失敗了,502 是代理在報告應用程式從來沒交出一個可用的答案。如果你框架的錯誤處理器有跑到,狀態碼就是 500,它的紀錄裡會留著那個例外,而代理會把那個回應原封不動轉出去。502 常常代表工作行程在任何處理器跑起來之前就死了,所以應用程式的紀錄是空的,唯一留下痕跡的只有代理的紀錄。 |
| 503 | 503 說的是伺服器「暫時」無法處理這個請求,而且通常是刻意的——維護模式、健康檢查把後端拉出輪替、限流器擋下來。RFC 9110 允許 503 帶上 `Retry-After`,502 沒有這種慣例。所以 503 代表有東西決定要拒絕你,502 代表根本沒有東西答得出話。 |
| 521 | 521 是 Cloudflare 自己的狀態碼,不是 IETF 的,而它存在的理由正是為了不讓這種情況被籠統地報成 502:它代表 Cloudflare 連到你來源機的連線被直接拒絕。看到 521 而不是 502,等於一步就把問題縮小到來源機的監聽或防火牆,而這就是 520 到 526 這一段當初被發明出來的原因。 |
nginx | nginx 在把 502 寫給用戶端之前,會先把原因寫進自己的錯誤紀錄,而那一行比三位數字具體太多了。`connect() failed (111: Connection refused)`、`upstream prematurely closed connection`、`upstream sent too big header`、`no live upstreams` 是四個完全不同的問題,但送到瀏覽器上長得一模一樣。 |
這些全都在你的瀏覽器裡跑,不會上傳任何東西。
查 502 的時候,通常也會順手看一下這幾個。
去讀回應標頭,不要讀那個網頁。curl -sS -o /dev/null -D - https://example.com/path 會只印標頭、不印內容,而 server、via 和 cf-ray 三個加起來,就能指出是哪一層回答的。有 cf-ray 的值代表這個回應是 Cloudflare 處理的,那個值也是他們客服會跟你要的編號。想把邊緣節點整個排除掉,就加上 --resolve example.com:443:203.0.113.10 再打一次:它會照樣送出原本的主機名稱和 SNI,但直接連到你指定的來源位址——答案變了,就代表邊緣節點和來源機講的不是同一件事。
不重要,而且絕對不要拿它來做判斷。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,會把值班的人送去堆疊裡錯的那一半。
502 還是卡住嗎?看完整的狀態碼對照表,或是回到上面,讀為你這一端寫的那一塊。