ByteScope

400

HTTP 400 Bad Request 怎麼修

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

伺服器根本不願意去解讀這個請求——請求行、某個標頭、訊息框架或 body 裡有東西格式錯了或太大——通常這代表你的應用程式碼一行都沒跑到。

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

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

用 curl 重現 400

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

curl
curl -sS -D - -o /dev/null -H "Cookie: pad=$(head -c 20000 /dev/zero | tr '\0' 'a')" https://example.com/

這行指令會送出一個超大的 `Cookie` 標頭——20 000 個位元組,遠遠超過 nginx 的 `large_client_header_buffers` 預設容許的 8k——所以只要對方跑的是 nginx 的預設值,它就是刻意重現公開網路上最常見的那種 400,而不是坐在那裡等它發生。`-D -` 印出回應標頭,`-o /dev/null` 把錯誤頁丟掉,狀態行就是結果。接著讀三件事。把 `-H` 拿掉再跑一次:回 200 就證明請求的形狀沒問題、只有大小不對,那是緩衝區的問題,不是語法問題。把位元組數對半砍,砍到 400 變成 200,你就量出了伺服器真正的標頭上限,而那個數字才是拿來跟 `large_client_header_buffers` 比對的。反過來,如果第一次跑就回 200,那不是指令有問題:擋在你前面的那層上限本來就比你送的量大,有些邊緣節點確實如此。把填充加倍到 40 000 個位元組,然後一直加倍到狀態碼翻過去為止——底下每一條讀法都預設你真的先看到過那個 400。再從輸出裡讀 `server:` 和有沒有 `cf-ray`,看是誰拒絕的,因為邊緣節點和來源機的上限可以不一樣,而只有其中一邊在你的設定檔裡。另一大類請跑 `curl -v http://example.com:443/`,它會對 TLS 埠送純文字 HTTP:nginx 對此回的是一個 400,body 寫著「The plain HTTP request was sent to HTTPS port」——認得這句話,可以省下一小時去翻那份什麼都沒寫的應用程式紀錄。

400 實際上在講什麼

RFC 9110 §15.5.1 把 400 定義成:伺服器因為察覺到某種用戶端錯誤而不處理這個請求,並舉了三個例子:請求語法格式錯誤、請求訊息框架不合法,以及帶有欺騙性的請求路由。這一句話涵蓋的兩種失敗其實住在完全不同的地方,而把它們分開,就是這件事的大半功夫。協定層級的 400,是伺服器或它前面那層邊緣節點在任何處理器被派工之前就寫出來的——RFC 9110 §7.2 要求伺服器對任何缺少 `Host` 欄位、帶了兩個以上、或帶了不合法 `Host` 的 HTTP/1.1 請求回 400;RFC 9112 §6.3 則把同時帶 `Content-Length` 和 `Transfer-Encoding` 的訊息定成一種和請求走私有關的錯誤,強化過的伺服器會直接拒絕,而不是自己猜。應用層級的 400 則是你自己的處理器順利解析完 body 之後拒絕了它。前者在你的應用程式紀錄裡連一行都不會留;後者就是紀錄裡的一行——這個不對稱,是你手上最快的測試方法。RFC 9110 對第二種情況還給了一個更精準的代碼:§15.5.21 定義了 422(Unprocessable Content),用在語法正確、但裡面的指示無法照做的內容上,而那才是多數「驗證失敗」回應真正的樣子。

400 最常被搞混的幾個代碼

容易搞混的怎麼分
422400 是給「伺服器解讀不了的請求」,422(Unprocessable Content,RFC 9110 §15.5.21)是給「解讀得完美無缺、但就是無法照做」的內容。一個沒通過你 schema 的 JSON body 其實解析得很順利——那是語意問題,不是語法問題——所以 422 是在告訴用戶端「payload 完整送到了,是值不對」,而那跟「你的請求格式壞了」是完全不同的修法。
414兩個都是大小限制,而它們分別坐在第一個換行的兩邊。nginx 的文件寫著,超過緩衝區的「請求行」回 414(URI Too Long,§15.5.15),過長的「標頭欄位行」回 400。所以 400 指向 cookie 和標頭,414 指向某個在迴圈裡被組出來的查詢字串——光看代碼就知道該縮的是請求的哪一半。
431431(Request Header Fields Too Large,RFC 6585 §5)才是為 cookie 過大這種情況存在的代碼,而且它比 400 有用得多。nginx 不用它——它回的是 400,body 寫「Request Header Or Cookie Too Large」——所以在公開網路上沒看到 431,完全不代表問題不在標頭。
nginxnginx 在把 400 寫給用戶端之前,會先把真正的原因寫進自己的錯誤紀錄,而那些訊息在狀態碼含糊的地方寫得很具體:`client sent too long header line` 是大小,`client sent invalid method while reading client request line` 是語法,而內部代碼 497——純文字送到 TLS 埠——到用戶端手上是一個 400,body 認得出來。一行紀錄可以取代一整個下午的猜測。
原因片語
Bad Request
類別
4xx 用戶端錯誤
定義出處
RFC 9110
標準
IETF 標準

站上可以搭配的工具

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

相關狀態碼

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

常見問題

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

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

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