【実務・中級編】HTTPステータスコード400(Bad Request)の発生原因とデバッグ – HTTPプロトコル・通信規格実践ガイド

400 Bad Requestの深淵:なぜ「君のリクエストは受け付けられない」のか

現場でエンジニアをしていると、避けては通れないのが「400 Bad Request」という壁です。サーバーのログにこのコードが並んでいるのを見ると、駆け出しの頃は「サーバーが壊れたのか?」と冷や汗をかいたものですが、シニアになった今なら断言できます。これはサーバーからの「お前の言い分(リクエスト)は、ルール違反だ」という、極めて正当な警告です。

今日は、HTTPプロトコルの基本に立ち返り、なぜこの400エラーが発生するのか、そして現場でどうやってこの「ルールの壁」を特定するかを、技術的な深掘りを交えて解説します。

—

1. 400 Bad Requestが投げられる「物理的な理由」

HTTPステータスコード400は、クライアント(ブラウザ、アプリ、APIクライアント)が送ったリクエストが、サーバー側の解析エンジン(Apache, Nginx, あるいはNode.js等のフレームワーク)にとって「意味不明」であるときに返されます。

ここで重要なのは、「サーバーのロジックに到達する前」に弾かれている可能性が高いということです。

代表的な発生原因

  • リクエストラインの構文エラー: HTTPメソッドとパスの間に空白がない、あるいはバージョン情報が不正。
  • 不正なヘッダー形式: ヘッダー名に許可されていない文字(コロンの欠落など)が含まれている。
  • ホストヘッダーの欠如: HTTP/1.1以降、`Host`ヘッダーは必須です。仮想ホスト環境では、これが無いとサーバーは「どのドメイン宛のリクエストか」を判断できず、即座に400を返します。
  • ヘッダーサイズの超過: 巨大なCookieや、不正に肥大化したカスタムヘッダー。

—

2. 通信フロー:パケットはどこで拒絶されるのか

クライアントが `GET / HTTP/1.1` を送った際、サーバーは以下のようなシーケンスでリクエストを解釈します。

1. ソケット接続(TCP 3-way handshake): ここは成功する。
2. リクエストラインのパース: サーバーが一行目を読み込み、プロトコルとして妥当か判断する。
3. ヘッダーのパース: 全てのヘッダーを読み込み、キー・値のペアを確認する。
4. アプリケーションへの引き渡し: ここで初めて「APIのロジック」が動く。

400エラーは、2または3のフェーズで発生します。つまり、アプリケーション側のバリデーションエラー(例:メールアドレスの形式が変)とは根本的に「レイヤー」が違うことを理解してください。

—

3. 実務的なデバッグ:犯人をどう特定するか

「なぜ400が出るのか?」と聞かれたら、まずは最小構成で再現させます。ブラウザのデベロッパーツールも良いですが、`curl` を使うのが最も純粋な通信を確認できるため確実です。

検証用のcurlコマンド

-v: 詳細な通信を表示 (verbose)
-H: ホストヘッダーを意図的に外したり、不正な形式にしてみる
curl -v -H “Host: example.com” http://localhost:8080/

意図的に不正なヘッダーを送ってみるテスト
curl -v -H “X-Invalid-Header: value with spaces and symbols!!!” http://localhost:8080/

Nginxで発生している場合(設定ファイル)

もしNginxで400が出ているなら、設定値を確認してください。特にヘッダーのサイズ制限が原因であることが多いです。

http {
# クライアントが送るヘッダーのバッファサイズ(デフォルトは1k程度)
# これを超えると 400 Bad Request を返します
large_client_header_buffers 4 8k;

# ヘッダー名のアンダースコアを許可するか(デフォルトはoff)
# underscores_in_headers on;
}

—

4. Pythonで再現コードを書く

APIの開発中、誤ったヘッダーをライブラリが自動生成してしまうことがあります。`requests` ライブラリを使って、あえて不正なヘッダーを送るコードを書いてみましょう。

import requests

不正なヘッダーの例:コロンが含まれない、あるいは制御文字が含まれる場合
headers = {
“Host”: “api.example.com”,
“InvalidHeaderName”: “value\r\nInjectedHeader: true” # CRLFインジェクションを試みる攻撃的リクエスト
}

try:
response = requests.get(“http://api.example.com/data”, headers=headers)
print(f”ステータスコード: {response.status_code}”)
except requests.exceptions.RequestException as e:
print(f”通信エラー: {e}”)

—

5. シニアエンジニアからの教訓

現場でこのエラーに遭遇したとき、多くの若手は「コードのどこが悪いか」を探し始めます。しかし、まず疑うべきは「通信経路の途中にいるプロキシやロードバランサー」です。

  • WAFの検知: 不正なシグネチャを検知して遮断している。
  • ロードバランサーの制限: ヘッダーの最大サイズがアプリサーバーとロードバランサーで異なっている。

デバッグの鉄則は、「一番近い場所から」です。

1. `curl` でlocalhostから直接叩く(サーバー単体の挙動)
2. ロードバランサー経由で叩く(境界の挙動)
3. 最後にパケットキャプチャ(`tcpdump`)で、実際に流れているバイト列を確認する。

400 Bad Requestは、ネットワークとアプリケーションの境界線にある「ゲートキーパー」からのメッセージです。そのメッセージを丁寧に解読すれば、障害解決までの時間は劇的に短縮されます。

プロトコルの仕様書(RFC 7230など)をたまに読み返すと、こうしたエラーが「仕様の範囲内」であることが分かって面白いですよ。エンジニアとしての深みは、こうした些細なエラーへの向き合い方で決まるのです。

コメント

タイトルとURLをコピーしました