ByteScope

400

HTTP 400 Bad Request 怎麼修

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

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

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

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

  • 前端工程師

    為什麼會看到它

    先知道有哪些事是你造不出來的會很有幫助。`Headers` 建構子在任何東西送出去之前就會擋掉不合法的標頭名稱和值,所以瀏覽器不會吐出一個語法壞掉的標頭——這樣就只剩兩種真實的樣子。第一種是大小:cookie 越積越多,直到某一行標頭超過伺服器的緩衝區,這時 nginx 會回一個 400,頁面上寫著「Request Header Or Cookie Too Large」。第二種是網址:一個沒編碼的空白、`{`、`}` 或 `|` 被直接插進樣板字串,原封不動送上線路,有些伺服器會當場拒絕那一行請求行。還有一個誤讀值得先擋掉——如果回 400 的是 CORS 預檢的那個 `OPTIONS`,瀏覽器會報成 CORS 失敗,於是你會為了一個「在 CORS 被考慮之前就被拒絕」的請求,花一整個下午去調 `Access-Control-Allow-Origin`。

    該怎麼做

    在網路面板裡對失敗的那一列按右鍵,複製成 cURL,拿到終端機跑。那裡一樣失敗,就代表請求本身格式就是錯的、跟瀏覽器無關——搜尋範圍當場砍一半。接著二分:在無痕視窗重新整理,或把那個網站的 cookie 刪掉,看 400 會不會消失;這一個測試就能把「太大」和「格式錯」分開,完全不用去讀伺服器設定。任何要插進網址的東西,路徑和查詢字串的部分請用 `encodeURIComponent` 編碼,不要相信輸入。還有,下結論之前先讀回應的 body:協定層級的 400 回來的是伺服器自己的 HTML 錯誤頁,你 API 的驗證錯誤回來的是你自己的 JSON——那就是「請求根本沒到應用程式」和「應用程式說不行」的差別。

  • 後端工程師

    為什麼會看到它

    先查出這個請求到底有沒有到你這裡,因為這兩種 400 的主人不一樣。nginx 的錯誤紀錄會直接把原因寫出來:`client sent too long header line` 是 cookie 過大那一種,`client sent invalid method while reading client request line` 則是請求真的壞掉了。大小上限就在 nginx 自己的指令裡——`client_header_buffer_size` 設第一個緩衝區,`large_client_header_buffers` 設其餘的;而且 nginx 的文件寫得很清楚:過長的「請求行」回 414,過長的「標頭」回 400。還有一種 nginx 專屬的 400 每次都能騙到人:對一個 TLS 監聽埠送純文字 HTTP,會產生 nginx 內部的 497,到用戶端手上變成一個 400,body 寫著「The plain HTTP request was sent to HTTPS port」——所以有人把 `https` 的 `s` 打掉、400 就跟著冒出來,那是連接埠配對的問題,不是 payload 的問題。剩下的是現代伺服器嚴格的框架檢查:重複的 `Content-Length`、冒號前面有空白,或是某個手寫用戶端、某台老舊代理送出的裸 LF 換行。

    該怎麼做

    先拿 request id 或路徑去 grep 應用程式紀錄。一行都沒有,就代表拒絕發生在你的程式碼前面,而原因只存在於代理的錯誤紀錄裡。如果是緩衝區,那就調大(`large_client_header_buffers 4 16k`),但要把它當成拖時間——一個會無限成長的 cookie 本身就是 bug,下一個尺寸遲早會來。用 `curl -v` 從線路層重現,然後把標頭一個一個加回去直到它壞掉,這樣指出來的是「哪個標頭」,不是「哪個請求」。如果那個請求是你自己的程式碼產的,去檢查框架欄位:絕對不要在設了 `Transfer-Encoding` 的同時又設 `Content-Length`,長度交給用戶端函式庫自己算。而當這個 400 真的是你的,把出錯的欄位寫進回應 body,並且問自己 422 是不是更誠實的代碼——RFC 9110 §15.5.21 的存在,就是為了那種「解析得乾乾淨淨、但仍然無法照做」的內容。

  • 一般使用者

    為什麼會看到它

    400 幾乎都代表你的瀏覽器連同網址一起送過去的某樣東西被拒絕了,而不是頁面不見了、或網站掛了。日常會遇到的原因有兩個:這個網站的 cookie 長得太大或壞掉了——cookie 每個請求都會送,而伺服器會拒絕標頭超過它容許大小的請求——以及網址在「你複製的地方」和「你貼上的地方」之間被弄壞了,通常是被信件折成兩行,或是從聊天訊息裡夾帶了一個空白。

    該怎麼做

    把那一個網站的 cookie 清掉再重新整理;Chrome 是點網址列上的鎖頭或調整鈕圖示,選「Cookie 和網站資料」,然後刪除。大部分人遇到的 400 這樣就解決了,而且跟「全部清光」不一樣,它不會把你其他網站也登出。如果這樣沒用,就仔細看網址有沒有空白、換行或缺字,然後重打一次網站首頁的網址,不要再用那條貼過來的連結。無痕視窗是最快的確認:它不帶你任何 cookie,所以在那裡打得開、在平常視窗打不開,就等於直接告訴你是上面兩個原因裡的哪一個。

用 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 會只印標頭、不印內容,而 server、via 和 cf-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 還是卡住嗎?看完整的狀態碼對照表,或是回到上面,讀為你這一端寫的那一塊。