22 人が閲覧(直近 30 日) プロトコル

HTTP error 400(Bad Request)とは?発生源の切り分けとnginx・Apache・IIS別の直し方

HTTP error 400(Bad Request)とは?発生源の切り分けとnginx・Apache・IIS別の直し方

HTTP error 400 は、サーバがリクエストを受け取ったものの、クライアント側に問題があると判断して処理を断ったときに返るコードです。厄介なのは、同じ400でも返した主体がアプリケーションのこともあれば、手前に立つリバースプロキシやIIS内部のHTTP.sysのこともある点でしょう。主体が違えば直す場所も変わるため、原因の推測より先に発生源の特定から入ります。この記事では切り分けの手順、サーバ実装ごとの閾値、APIを作る側の返し方までを、公式ドキュメントの記述と実行できるコマンドで押さえていきます。400以外のコードも横断で確認したい場合はHTTPステータスコード一覧とcurlエラーコードの違いを参照してください。

まとめ|HTTP 400は誰が返したのかを先に特定してから直す手順

400 Bad Request は「サーバがクライアントエラーとみなした」ことだけを示すコードで、原因の内訳までは含みません。MDNの400 Bad Requestの解説は、理由として不正なリクエスト構文・不正なメッセージフレーミング・不正なリクエストルーティングの3つを挙げ、クライアントはリクエストを変えずに再送しても同じ結果になると想定すべきだと書いています。つまり、リトライで直る種類のエラーではありません。

直す順序は3段階です。第一に発生源の特定——アプリのアクセスログに該当リクエストが無ければ前段が返しています。第二に理由の特定——前段ならヘッダやリクエストラインの長さ超過、不正なヘッダ名、Hostヘッダの不一致が大半です。第三に対処の選択——閾値を上げるのは緩和策なので、肥大化そのものを止める手当てとセットで考えてください。

閾値の既定値は実装ごとに違います。nginxはヘッダフィールド1本あたり8k、Apacheは8190バイト、Tomcatはリクエストライン+ヘッダ合計で8192バイト、IISのHTTP.sysはフィールド1本あたり16384バイトです。

HTTP error 400が示す意味をRFC 9110の定義から正確に押さえる

400の定義と、リクエストを変えずに再送しても同じ結果で失敗する理由

HTTPのステータスコードの意味はRFC 9110 HTTP Semanticsが規定しています。2022年6月に発行され、STD 97としてインターネット標準に位置づけられた文書です。ここで400は、サーバがリクエストを処理できない、あるいは処理しない理由がクライアント側にあると認識した場合に返すコードとして定義されています。

押さえておきたいのは、400が「サーバの認識」を伝えるコードだという点でしょう。筋の通らないリクエストが届いた事実は伝わりますが、どこがどう筋が通らなかったかは本文に書かれていない限り分かりません。502や503のようにサーバ側の一時的な事情で返るコードとは、対処の方向が正反対になります。

コードの意味づけがどの仕様に基づくかを確認したいときは、IANAのHTTP Status Code Registryが公式の登録簿になります。コードと参照仕様の対応が一覧で載っており、独自コードを名乗る前に既存の登録を確認する用途にも使えます。

401・403・404・413・414・422との線引きを判断できるようにする

400は4xx系のうち最も範囲が広く、他のコードで表せない場合の受け皿になりがちです。線引きは次のとおりです。認証情報が無い、または無効なら401。認証は通っているが権限が足りないなら403。指定されたリソースが存在しないなら404。ボディが大きすぎるなら413、URIが長すぎるなら414。構文としては正しいがアプリの業務ルールで受け付けられないなら422。

実務で迷うのは400と422の境目でしょう。ここは「JSONとしてパースできたか」で切ると運用が安定します。パースに失敗した、必須フィールドの型が違うなど、そもそも解釈できない段階なら400。パースは成功していて値の意味が業務上あり得ない(開始日が終了日より後、在庫を超える数量)なら422です。この基準はチームで先に文書化してください。

なお、この線引きはHTTPを直接使う場合の話です。gRPCは独自のステータス体系を持ち、HTTPのコードと一対一では対応しません。対応表とリトライ可否はgRPCのステータスコードの意味と選び分けにまとめてあります。

400を返したのは前段かアプリかをcurlとログで切り分ける手順

curl -v で生のやり取りを見て応答の出どころを推定する

最初にやるのは、ブラウザではなくcurlで同じリクエストを再現し、応答ヘッダと本文を生で見ることです。ブラウザの画面表示はエラーページに置き換えられている場合があり、判断材料が削られます。

$ curl -v -o response.txt -D headers.txt "https://api.example.com/v1/orders"

> GET /v1/orders HTTP/1.1
> Host: api.example.com
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Server: nginx
< Date: Sat, 06 Sep 2026 03:12:44 GMT
< Content-Type: text/html
< Content-Length: 157
< Connection: close
<

$ cat response.txt
<html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx</center>
</body>
</html>

この応答は前段のnginxが返したものです。根拠は3つで、Content-TypeがJSONを返すはずのAPIなのにtext/htmlであること、本文がnginxの既定エラーページの形をしていること、Connection: closeで接続が切られていることです。本文が自前のJSONなら、リクエストはアプリまで到達していると読めます。

もう一段確実にしたい場合は、意図的に長いヘッダを付けて閾値を跨ぐか確認します。次のコマンドは16KBのダミーヘッダを1本足して送るものです。既定値のnginxなら400が返り、ヘッダを外せば200が返るなら、長さ超過が原因だと確定できます。

$ python -c "print('X-Dummy: ' + 'a'*16000)" > /tmp/bigheader.txt
$ curl -s -o /dev/null -w "%{http_code}\n" \
    -H "@/tmp/bigheader.txt" \
    "https://api.example.com/v1/orders"
400

$ curl -s -o /dev/null -w "%{http_code}\n" \
    "https://api.example.com/v1/orders"
200

アクセスログに残らない400は前段で止められている証拠になる

curlの結果と合わせて、サーバ側のログを突き合わせます。ここで見るのは「アプリのアクセスログに、その時刻の該当リクエストが記録されているか」の一点です。記録が無ければ、リクエストはアプリまで届かずに前段で弾かれています。

nginxの場合、長さ超過で弾かれたリクエストはアクセスログに載っても$requestが-になったり、リクエストラインが途中で切れた形で記録されたりします。パースに失敗しているのでログに書ける情報が無いためです。エラーログ側にはclient sent too long header lineのような行が出ます。

$ sudo tail -n 50 /var/log/nginx/error.log | grep -i "too long"
2026/09/06 03:12:44 [info] 1234#1234: *5678 client sent too long header line:
  "Cookie: session=..." while reading client request headers,
  client: 203.0.113.10, server: api.example.com, request: "GET /v1/orders HTTP/1.1"

$ sudo grep " 400 " /var/log/nginx/access.log | tail -n 5

IISを使っている場合は、Microsoft LearnのIISでのHTTP 400エラーのトラブルシューティングが示すとおり、HTTP.sysが弾いた要求は C:\Windows\System32\LogFiles\HTTPERR\httperr.log に理由フレーズつきで記録されます。同記事のシナリオでは、ヘッダ長超過が FieldLength として残りました。逆に該当行が無ければ、IISのモジュールかアプリのランタイム側が400を設定したと読めます。

アプリまで到達していて、なおかつ400が返っているなら、次に疑うのはフレームワークの入口で動く検証処理です。フォーム送信でトークン検証に落ちて400や419が返る例は典型で、切り分け方はInvalid CSRF tokenの意味と直し方にまとめてあります。

リクエストヘッダのサイズ超過で400になる仕組みと各実装の既定値

nginxはヘッダ1本が8kを超えると400・リクエストラインなら414

nginx公式のngx_http_core_moduleドキュメントは、この挙動を明文で書いている数少ない資料です。client_header_buffer_sizeの既定は1kで、まずこのバッファで読みます。リクエストラインかヘッダフィールドが1kに収まらない場合はlarge_client_header_buffersで確保した大きいバッファへ移り、その既定は4 8k——8KBのバッファを最大4本、という意味です。

ここが分岐点になります。同ドキュメントは、リクエストラインが1バッファのサイズを超えると414(Request-URI Too Large)、リクエストヘッダフィールドが超えると400(Bad Request)を返すと明記しています。つまりURLが長すぎるなら414、Cookieなどヘッダ1本が長すぎるなら400で、返ってきたコードから逆算できるでしょう。

http {
    # 最初に読むバッファ(既定 1k)
    client_header_buffer_size   1k;

    # 大きいヘッダ用(既定 4 8k)。ヘッダ1本の上限が第2引数
    large_client_header_buffers 4 16k;

    # ボディの上限(既定 1m)。超過時は 413 で 400 ではない
    client_max_body_size        10m;
}

注意点として、バッファは本数ではなくサイズが効きます。4 8kは合計32KBまで受けられる意味ではなく、ヘッダ1本が8KBを超えたら本数に余裕があっても400です。Cookieは複数あっても1本のヘッダに連結されて送られるため、この上限に当たりやすい構造になっています。

ApacheとTomcatはリクエストライン・ヘッダの上限が8190前後で並ぶ

Apache HTTP Server 2.4のcoreモジュールでは、LimitRequestLineの既定が8190、LimitRequestFieldSizeの既定が8190、ヘッダの本数を制限するLimitRequestFieldsの既定が100です。nginxの8kとほぼ同水準で、実運用の閾値としては似た位置に並びます。

# Apache 2.4(httpd.conf / server config・virtual host)
LimitRequestLine      16380
LimitRequestFieldSize 16380
LimitRequestFields    100

Tomcatを直接ぶら下げている構成では、Tomcat 10.1のHTTP Connector設定を見ます。maxHttpHeaderSizeの既定は8192(8 KiB)で、これがmaxHttpRequestHeaderSizeの既定値を供給します。同ドキュメントが釘を刺しているのは、この値がリクエストごとに全量割り当てられる点です。1MBに広げて同時100リクエストを捌けば、ヘッダだけでヒープを100MB使う計算になります。

<!-- Tomcat 10.1: conf/server.xml -->
<Connector port="8080"
           protocol="HTTP/1.1"
           maxHttpHeaderSize="16384"
           rejectIllegalHeader="true"
           allowHostHeaderMismatch="false"
           maxParameterCount="10000" />

Tomcatには長さ以外に400を返す属性が2つあります。rejectIllegalHeaderは、ヘッダ名がトークンとして不正な場合などに400で拒否するかを決める設定で、既定はtrueです。allowHostHeaderMismatchは、リクエストラインのホストとHostヘッダが食い違うリクエストを拒否する設定で、既定はfalse——つまり不一致は拒否されます。同ドキュメントによれば、どちらもTomcat 11以降では設定自体が削除され、拒否する側に固定される予定です。回避目的で緩めている環境は、版を上げた時点で400が復活すると考えて移行計画に入れてください。

IISはHTTP.sysのMaxFieldLengthが既定16384で他より広い

IISの場合、閾値を持つのはIIS本体ではなくカーネルモードのHTTP.sysです。Microsoft Learnの表によれば、MaxFieldLengthの既定は16384、有効な値の範囲は64〜65534(64k − 2)バイトで、各ヘッダの上限として効きます。同表はこの制限がURLでは約32,000文字に相当するとも書いており、nginxやApacheの8k前後より広い設定です。

REM HTTP.sys のレジストリキー(変更後は再起動が必要)
REM HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\HTTP\Parameters

reg query "HKLM\SYSTEM\CurrentControlSet\Services\HTTP\Parameters" /v MaxFieldLength

REM httperr.log の該当行を確認する(理由フレーズが右端に出る)
REM 2012-11-14 20:36:36 HTTP/1.1 GET /1234567890/time.asp 400 FieldLength

Microsoft Learnの記事が挙げるもう1つの典型シナリオが、Kerberos認証とActive Directoryの組み合わせです。多数のADグループに所属する利用者は認証トークンが肥大化し、その分だけヘッダが膨らんで Bad Request (Request Header Too Long) に当たります。特定の社員だけ400になる、管理職だけ入れない、といった症状が出たらこの線を疑ってください。所属グループ数という業務側の要因が閾値を超えている状態で、アプリのコードを読んでも原因は見つかりません。

Cookieの肥大化とヘッダ上限の関係を数えて恒久策を決める道筋

Cookieは1本のヘッダに連結される|閾値までの余裕を数える方法

400の相談で最も多い経路がCookieの肥大化です。同一ドメインに複数のCookieが設定されていても、送信時はCookie:という1本のヘッダに名前=値; 名前=値; ...の形で連結されます。上限に当たるのは、この連結後の長さです。

余裕を数えるには、開発者ツールのネットワークタブでリクエストヘッダのCookie行の長さを見るのが早道でしょう。JavaScriptから概算する手もありますが、document.cookieはHttpOnly属性のCookieを含まないため、実際のヘッダ長より小さく出る点に注意してください。

// ブラウザのコンソールで概算(HttpOnly分は含まれない)
new Blob([document.cookie]).size;

// サーバ側で実測する(Node.js / Express の例)
app.use((req, res, next) => {
  const raw = req.headers.cookie || '';
  if (Buffer.byteLength(raw, 'utf8') > 4096) {
    console.warn('cookie header size', Buffer.byteLength(raw, 'utf8'));
  }
  next();
});

閾値の目安は、前段で最も狭い実装に合わせます。nginxが前段でヘッダ上限8KB、Cookie以外のヘッダで1〜2KB使うなら、Cookieに割ける余裕は6KB前後です。実測が4KBを超えた時点で警告を出しておけば、本番で400が出る前に気づけます。

上限引き上げが緩和策にとどまる理由と、設計側で減らす3つの手当て

設定値を広げれば目の前の400は止まりますが、それは閾値を先送りしただけで、増え続ける前提は変わりません。Tomcatの例で見たとおり、ヘッダバッファはリクエストごとに割り当てられるため、広げた分だけ同時接続時のメモリ消費が増えます。CDNやWAFを挟んでいる場合、自社サーバだけ広げても手前で切られる構成もあり得ます。

減らす側の手当ては3つです。第一に、セッションに載せる情報をIDだけに絞り、実体はサーバ側のストアに置く。第二に、認証トークンのクレームを見直す——JWTに権限一覧を全部入れると簡単に数KBに達するため、ロール名だけにして詳細は都度引く。第三に、解析タグやA/Bテストツールが設定するCookieを棚卸しする。運用が長いサイトほど、使われていないツールのCookieが残っています。

APIとして400を返す側で400と422をどう選び分けるかを決める

パースできたかどうかで線を引くと、実装者ごとの判断の揺れが止まる

ここまでは400を受け取る側の話でしたが、APIを作る側に回ると「どの場合に400を返すか」を決める必要が出てきます。基準を言語化しないまま実装すると、必須項目の欠落を400で返すエンドポイントと422で返すエンドポイントが混在し、クライアント側のエラー処理が場当たりになります。

おすすめの基準は前述のとおり、リクエストを解釈できた段階で切ることです。JSONとして壊れている、Content-Typeが宣言と違う、必須のクエリパラメータが型変換できない——ここまでは400。解釈は済んでいて、値の組み合わせが業務ルールに反する場合は422。この線引きなら実装者による判断のぶれがほぼ消えます。入力検証そのものの実装パターンはバリデーションの考え方と実装にまとめてあります。

400の本文はproblem+jsonで返し、原因を機械可読にする

400を返すとき、本文が空だとクライアント側は何を直せばよいか分かりません。標準的な書き方がRFC 9457 Problem Details for HTTP APIsで、2023年7月に発行され旧RFC 7807を置き換えた文書です。メディアタイプはapplication/problem+jsonで、type・status・title・detail・instanceのメンバーを持ちます。

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
X-Request-Id: 7f3c1b9e-2a44-4c07-9d51-8ab0e4c15d33

{
  "type": "https://api.example.com/probs/malformed-json",
  "title": "Request body could not be parsed as JSON.",
  "status": 400,
  "detail": "Unexpected token at line 3, column 18.",
  "instance": "/v1/orders",
  "requestId": "7f3c1b9e-2a44-4c07-9d51-8ab0e4c15d33"
}

typeは問題の種類を識別するURI、titleは種類の要約、detailはこの発生に固有の説明という役割分担です。titleを発生ごとに変えると、クライアント側が種類で分岐できなくなります。付番体系や多言語メッセージ、スタックトレースの漏洩防止まで含めた設計はAPIエラーレスポンス設計の実装ガイドで扱っています。

リクエストIDを載せて、問い合わせから該当ログへ一直線に辿る

400の問い合わせが来たとき、時刻とURLだけでログを探すのは骨が折れます。レスポンスヘッダと本文の両方に一意なリクエストIDを載せておけば、問い合わせの画面キャプチャからサーバログの1行へ直接辿り着けます。前段のnginxで発行してX-Request-Idとして渡し、アプリ側は受け取った値を引き継ぐ設計にしてください。

server {
    location / {
        # nginx が発行するリクエストID(1.11.0以降の $request_id)
        proxy_set_header X-Request-Id $request_id;
        add_header       X-Request-Id $request_id always;
        proxy_pass       http://app_backend;
    }
}

ここでalwaysを付けている理由が400の文脈では効いてきます。付けないとエラー応答にヘッダが載らず、肝心の失敗時にIDが出ません。ただし前段がパースに失敗して400を返す場合はアプリまで到達しないため、nginxのログ形式にも$request_idを含めておく必要があります。

ヘッダ上限を引き上げてよい場面と引き上げてはいけない場面の線引き

引き上げてよい3条件と、引き上げるべきでない2条件を先に決める

運用で判断が割れるのがここです。設定値の引き上げは実行が簡単で効果も即座に出るため、原因を確認しないまま恒久策として採用されがちでしょう。線を引いておきます。

引き上げてよいのは次の3条件のいずれかに当てはまるときです。第一に、ヘッダが大きい理由が業務要件として妥当で減らせない場合——先に挙げたKerberosのADグループ数がこれに当たります。所属グループを減らすのは技術判断ではなく組織の話で、システム側で吸収するほうが筋が通ります。第二に、外部システムから受け取るリクエストで、送信側の仕様を自社で変更できない場合。第三に、減らす改修の期間を確保するための時限措置として、期限と見直し時期を決めて実施する場合です。

逆に引き上げるべきでないのは2条件です。第一に、原因の特定が済んでいない場合——長さ超過だと確認せずに広げると、別の原因(不正なヘッダ名、Hostヘッダの不一致、URLのエンコード漏れ)を見落とします。第二に、自社が積んでいるCookieやトークンが原因で削れる余地がある場合。ここで広げると、次の機能追加でまた同じ場所に当たります。

400を監視でどう扱うか|アラートを鳴らす基準と鳴らさない基準

400はクライアント起因のコードなので、1件出るたびにアラートを鳴らすと通知が埋もれます。URLを手入力した利用者や古いブックマークでも400は出るためです。監視は件数ではなく比率と傾向で見るのが扱いやすいでしょう。

具体的には3つの見方を組み合わせます。第一に、総リクエストに対する400の比率が普段の水準から跳ねたら通知する。第二に、同一エンドポイントに集中する400は実装の不具合を疑い、分散しているならクライアント側の事情と見る。第三に、前段が返した400は別枠で数える——これが増えていれば環境側の変化が起きています。

加えて、認証済みの利用者から出る400は優先度を上げて扱ってください。ログインを済ませた人が操作の途中で400に当たっているなら、原因は自社の実装か設定にある確率が高くなります。切り分けと監視設計まで含めて整理したい場合は、API開発・システム連携の窓口で現行構成を踏まえた検討ができます。

よくある質問

ブラウザで400が出たとき、利用者側で試せることはありますか?

該当サイトのCookieを削除してから再アクセスする方法が最も効きます。400の多くはCookieの肥大化か破損が原因で、削除すれば発行し直されるためです。次に試すのはURLの確認で、全角文字や余分な記号が混ざっていないかを見てください。何度試しても直らないなら、サイト運営側の設定に原因があると考えて構いません。

URLに日本語やスペースが含まれていると400になりますか?

エンコードされていなければなり得ます。リクエストラインに生のスペースが入ると、HTTPの構文上そこでリクエストラインが終わったと解釈され、パースに失敗して400が返ります。日本語や記号は%E3%81%82のようなパーセントエンコードで送る必要があり、ブラウザは自動で行いますが、プログラムから組み立てる場合は明示的なエンコード処理が要る点に注意してください。

ローカルでは動くのに本番だけ400になるのはなぜですか?

前段の構成が違うためです。ローカルはアプリケーションサーバに直接アクセスしていることが多く、本番ではnginxやALB、CDN、WAFが手前に入ります。それぞれ独自のヘッダ上限や検査ルールを持つため、アプリに届く前に弾かれます。本番でアプリのアクセスログに該当リクエストが残っているかを確認してください。加えて、本番だけ認証Cookieが大きい、SSO経由でトークンが乗る、といった差分も定番の原因です。

400と422はどちらを返すのが正しいのでしょうか?

どちらか一方が常に正しい性質のものではなく、チームで基準を決めて統一するほうが効きます。おすすめはリクエストを解釈できたかで切る方法で、パースに失敗したら400、パースは成功して値の意味が業務上あり得ないなら422です。クライアントが自社のフロントエンドだけなら、すべて400に寄せて本文で理由を伝える設計も成り立ちます。

nginxのlarge_client_header_buffersはどこまで広げてよいですか?

ヘッダの実測値に対して2倍程度の余裕を持たせる範囲に留めるのが無難でしょう。既定の4 8kから4 16kへ上げる程度なら影響は限定的ですが、64kや128kまで広げると同時接続数に比例してメモリを消費します。広げる前に、そもそも何バイト来ているのかをエラーログで実測してください。値を決める根拠が無いまま広げるのが一番避けたい進め方です。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

RELATED POSTS 関連記事

目次