ByteScope

400

HTTP 400 Bad Request の直し方

4xx クライアントエラーIETF 標準RFC 9110

サーバーがリクエストの解釈自体を拒みました。リクエスト行・ヘッダー・メッセージの区切り・ボディのどこかが壊れているか大きすぎるということで、たいていの場合、アプリケーションのコードは 1 行も走っていません。

400 が出ている理由と、やるべきこと

同じ 3 桁でも、リクエストのどちら側にいるかで別々の問題になります。自分に当てはまるブロックを読んでください。

  • フロントエンド開発者

    これが出ている理由

    自分には起こせないことを知っておくと近道になります。`Headers` コンストラクタは不正なヘッダー名と値を送信前に弾くので、構文的に壊れたヘッダーをブラウザが出すことはありません。残る現実的な形は 2 つです。1 つ目はサイズ。Cookie が膨らみ続けて 1 本のヘッダー行がサーバーのバッファを超え、nginx が「Request Header Or Cookie Too Large」と書かれたページを 400 で返す、というやつです。2 つ目は URL。テンプレート文字列にそのまま埋め込まれた生の空白や `{`・`}`・`|` はそのまま線に出ていき、サーバーによってはリクエスト行の時点で受け付けません。もう一つ、先回りしておく価値のある誤読があります。400 を返しているのが CORS のプリフライト `OPTIONS` だった場合、ブラウザは CORS の失敗として報告するので、CORS を検討される前に弾かれたリクエストのために `Access-Control-Allow-Origin` を睨んで午後を溶かすことになります。

    やること

    ネットワークパネルで失敗している行を右クリックして cURL としてコピーし、ターミナルで実行してください。そこでも失敗するなら、リクエスト自体が壊れていてブラウザは話に関係ありません。これだけで探索範囲が半分になります。次に二分します。プライベートウィンドウでリロードするか、そのサイトの Cookie を消して、400 が消えるかどうかを見る。このテスト 1 回で、サーバーの設定を一行も読まずに「大きすぎる」と「壊れている」が分かれます。URL に埋め込む値は、入力を信用せずパスやクエリの断片として `encodeURIComponent` に通してください。そして結論を出す前に応答のボディを読むこと。プロトコル階層の 400 はサーバー自身の HTML エラーページで返り、自分の API のバリデーションエラーは自分の JSON で返ります。その差がそのまま「リクエストがアプリケーションまで届かなかった」と「アプリケーションが拒んだ」の差です。

  • バックエンド開発者

    これが出ている理由

    まず、そのリクエストが自分のところまで届いたのかを確かめてください。2 種類の 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 がもう 1 つ、毎回誰かを引っかけます。TLS のリスナーに平文 HTTP を送ると nginx 内部の 497 になり、クライアントには「The plain HTTP request was sent to HTTPS port」というボディの 400 として届きます。誰かが `https` の `s` を落とした瞬間に出る 400 は、ペイロードの話ではなくポートの組み合わせの話です。残りは最近のサーバーの厳格なフレーミング検査です。重複した `Content-Length`、コロンの前の空白、手書きのクライアントや古いプロキシが出す裸の LF 改行。

    やること

    まずリクエスト ID かパスでアプリケーションのログを grep してください。1 行も無ければ拒否はコードの手前で起きていて、理由が存在するのはプロキシのエラーログだけです。バッファが原因なら上げてください(`large_client_header_buffers 4 16k`)。ただしそれは時間を買っただけだと思うこと。無制限に育つ Cookie はそれ自体がバグで、次のサイズは必ずやってきます。`curl -v` で線の上から再現し、ヘッダーを 1 本ずつ戻していけば、犯人のリクエストではなく犯人のヘッダーが特定できます。リクエストを生成しているのが自分のコードなら、フレーミングのフィールドを確認してください。`Content-Length` と `Transfer-Encoding` を同時に設定しないこと、長さの計算はクライアントライブラリに任せること。そして 400 が本当に自分のものなら、失敗したフィールドを応答ボディに入れたうえで、422 のほうが正直ではないかを考えてください。RFC 9110 §15.5.21 は、きれいにパースできたのに実行できない内容のために存在します。

  • 利用者

    これが出ている理由

    400 はほぼ常に、ページが無いとかサイトが落ちているという話ではなく、アドレスと一緒にブラウザが送った何かが拒否された、という意味です。日常的な原因は 2 つです。1 つはそのサイトの Cookie が大きくなりすぎたか壊れたこと。Cookie は毎回のリクエストに付いていくので、ヘッダーが許容量を超えたリクエストはサーバーに拒まれます。もう 1 つは、コピー元から貼り付け先までの間にアドレスが壊れたこと。メールの中の改行や、チャットから紛れ込んだ空白がその典型です。

    やること

    そのサイトだけの Cookie を消してリロードしてください。Chrome ならアドレスバーの鍵かスライダーのアイコン →「Cookie とサイトデータ」→ 削除です。世の中で出会う 400 の大半はこれで直りますし、全部を消すのと違って他のサイトからサインアウトされることもありません。それで直らないなら、アドレスに空白・改行・欠けた文字がないかをよく見て、貼り付けたリンクを使い回さずサイトのトップのアドレスを打ち直してください。プライベートウィンドウがいちばん速い確認になります。あなたの Cookie を一切持たないので、そこでは開けて普段のウィンドウでは失敗するなら、原因は 2 つのうちどちらなのかがそれで確定します。

curl で 400 を再現する

失敗した URL に対してそのまま実行してください。エラーページを表示せずにステータスだけを出すので、ブラウザが描画したものではなくサーバーが言ったことが見えます。

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

これは巨大な `Cookie` ヘッダーを 1 本だけ送ります。20,000 バイト、nginx の `large_client_header_buffers` が既定で許す 8k をゆうに超える大きさなので、nginx を既定のまま動かしている相手なら、公開ウェブでいちばんよくある 400 を待たずに意図的に再現できます。`-D -` が応答ヘッダーを出し、`-o /dev/null` がエラーページを捨て、ステータス行が結果です。読み方は 3 つ。まず `-H` を外して実行し直す。200 になるなら、リクエストの形は正しくサイズだけが問題だったということで、これは構文ではなくバッファの話です。次にバイト数を半分ずつ減らして 400 が 200 に変わる点を探せば、そのサーバーの実際のヘッダー上限が測れます。これが `large_client_header_buffers` と突き合わせるべき数字です。逆に 1 回目から 200 が返ってきた場合、コマンドが間違っているのではありません。手前にある上限が送った量より単に大きいだけで、実際そういうエッジもあります。パディングを 40 000 バイトに倍増し、ステータスが変わるまで倍にし続けてください。ここから先の読み方はどれも、400 を実際に見たことを前提にしています。そしてダンプの `server:` と `cf-ray` の有無から、拒否したのが誰かを読みます。エッジとオリジンで上限が違うことがあり、自分の設定に書いてあるのはそのうち片方だけだからです。もう一つの大きな系統には `curl -v http://example.com:443/` を使います。TLS のポートに平文 HTTP を送るもので、nginx はこれに「The plain HTTP request was sent to HTTPS port」というボディの 400 で答えます。この一文を知っているだけで、何も書かれていないアプリケーションログを読む 1 時間が浮きます。

400 が実際に意味していること

RFC 9110 §15.5.1 は 400 を、クライアント側の誤りと受け取れる何かのせいでサーバーがリクエストを処理しない状態と定義し、例として壊れたリクエスト構文・不正なメッセージフレーミング・欺瞞的なリクエストルーティングの 3 つを挙げています。この一文は、住んでいる建物がまったく違う 2 種類の失敗をまとめて指していて、その 2 つを切り分けることが作業のほとんどです。プロトコル階層の 400 は、ハンドラが呼ばれる前にサーバーか手前のエッジが書きます。RFC 9110 §7.2 は、`Host` フィールドの無い HTTP/1.1 リクエスト、複数持つリクエスト、不正な値を持つリクエストに 400 を返すことをサーバーに要求していますし、RFC 9112 §6.3 は `Content-Length` と `Transfer-Encoding` を両方運ぶメッセージをリクエストスマグリングに直結する誤りと定めていて、堅く作られたサーバーは推測せずに拒否します。もう一方、アプリケーション階層の 400 は、パースには成功したボディをあなた自身のハンドラが突き返したものです。前者はアプリケーションのログに 1 行も残らず、後者は残る——この非対称が、いま使える中でいちばん速い判定です。なお RFC 9110 は後者向けにもっと鋭いコードを用意しています。§15.5.21 の 422 (Unprocessable Content) は、構文は正しいのに指示に従えない内容のためのもので、世の「バリデーション失敗」応答の実態はたいていこちらです。

400 と間違えられやすいコード

紛らわしいコード見分け方
422400 はサーバーが解釈できなかったリクエストのため、422 (Unprocessable Content, RFC 9110 §15.5.21) は完全に解釈できたのに実行できない内容のためのコードです。スキーマ検証に落ちた JSON ボディはパース自体は成功していて、問題は構文ではなく意味です。422 ならクライアントに「ペイロードは無傷で届いた、値が違う」と伝わり、「リクエストが壊れていた」とは別の対処になります。
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 はこれを使わず、「Request Header Or Cookie Too Large」というボディを付けた 400 で答えます。公開ウェブで 431 を見かけないことは、ヘッダーが原因かどうかについて何の情報にもなりません。
nginxnginx はクライアントに 400 を書く前に、本当の理由を自分のエラーログへ書いています。ステータスコードが曖昧なぶん、こちらの文言は具体的です。`client sent too long header line` はサイズ、`client sent invalid method while reading client request line` は構文、そして内部コード 497——TLS ポートに届いた平文——は、まぎれもないボディを付けた 400 としてクライアントに出ます。ログ 1 行が、当て推量の午後を丸ごと置き換えます。
理由句
Bad Request
クラス
4xx クライアントエラー
定義
RFC 9110
標準
IETF 標準

このサイトのツール

どれもブラウザ内で完結します。アップロードは発生しません。

関連するステータスコード

400 を調べているとき、ついでに読むことになりがちなコードです。

よくある質問

このステータスコードを出したのがどのサーバーかを知るには?

ページではなく応答ヘッダーを読んでください。curl -sS -o /dev/null -D - https://example.com/path が本文なしでヘッダーだけを出し、server・via・cf-ray の 3 つで、答えたのがどの層かが分かります。cf-ray の値があれば応答を扱ったのは Cloudflare で、その値はサポートが聞いてくる ID です。エッジを完全に外して確かめたいなら、--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 はカードに二重で課金しかねません。すでに失敗しているサーバーへの再試行の嵐は、短い障害を長い障害に変える典型的な経路です。

自分のサーバーが返すステータスコードは変えられますか?

変えられます。守るべき規則は 1 つだけ、そのコードが本当であること。応答を自動で読むもの——検索エンジン、監視、キャッシュ、クライアント側の再試行ロジック——はすべて数字だけで判断し、ページの中身は一切見ません。エラーページを 200 で返せば自分の監視から障害が隠れますし、無いページを 200 で返せばエラーが本文として索引されますし、単に不正なリクエストに 500 を返せば、当番の人をスタックの間違った半分へ送り込むことになります。

400 でまだ詰まっているなら、ステータスコード一覧をすべて見る。あるいは上に戻って、自分の立ち位置に向けて書かれたブロックを読んでください。