デカすぎるペイロードに鉄槌を!HTTP 413 (Payload Too Large) と `Connection: close` の深層
「おい、新人の〇〇くん、ちょっと画面を見てくれ」
深夜のオペレーションルーム。監視モニターに赤々と点灯したアラートを指さしながら、私はコーヒーカップを置いた。
Web APIの負荷テストの最中、クライアント側から送信された巨大なJSONペイロードに対して、サーバーが突如として冷酷な切断を下す。ブラウザやAPIクライアントには `413 Payload Too Large` という見慣れない、しかし極めて重要なステータスコードが返されている。
API設計やインフラ運用において、クライアントが「どれだけデカいデータを送りつけてきてもサーバー側でよしなに受け止めてくれるだろう」という性善説は、ネットワークの世界では通用しない。DoS攻撃の温床になり、メモリを食いつぶし、最悪の場合はカーネルパニックを引き起こす。
今回は、この HTTP 413 (Payload Too Large) の正体に迫り、RFCが定義する厳格なルール、そして背後で何が起きているのかを、シニアエンジニアの視点で紐解いていこう。
—
1. HTTP 413ステータスコードの正体とRFCの思想
HTTPステータスコード `413 Payload Too Large`(旧仕様HTTP/1.1では `413 Request Entity Too Large` と呼ばれていた)は、「サーバーが処理しきれないほど巨大なリクエストボディをクライアントが送信したため、サーバーがリクエストの処理を拒否した」 ことを示す。
ここで重要なのは、これが単なる「エラー」ではなく、サーバー側の防衛機制(Self-Defense Mechanism) であるという点だ。
RFC 9110が定める厳格な挙動
最新のHTTPセマンティクスを定義する RFC 9110 において、413ステータスコードを受け取ったサーバー側の振る舞いには、極めて興味深い規定がある。
> 「サーバーは、リクエストの処理を継続できず、あるいは継続したくない場合、リクエストボディの読み込みを打ち切り、`413` レスポンスを返すべきである。さらに、サーバーがこれ以上のリクエスト処理(または同一接続での通信継続)を望まない場合、`Connection: close` ヘッダーを送信してTCPコネクションを切断するべきである。」
つまり、413を返した時点で、サーバーはクライアントに対してこう宣言しているのだ。
「お前の送ってきたデータはデカすぎる。これ以上話すことはない。この接続は切る!」
—
2. パケットの世界:なぜ `Connection: close` が強制されるのか?
ここでネットワークの低レイヤー、TCPの挙動に目を向けよう。HTTP/1.1のデフォルト挙動は Keep-Alive(持続的接続) だ。1本のTCPコネクションを張りっぱなしにして、その上で何回もHTTPリクエストとレスポンスを往復させる。
しかし、もしクライアントが数百MBもある巨大なファイルを、Keep-Alive中のTCPコネクションに向けてダラダラと送信し始めたらどうなるだろう?
無慈悲な切断のシーケンス
[Client] [Nginx / Reverse Proxy]
| |
|— (1) POST /upload (Content-Length: 500MB) ————->|
| | — (2) 制限値(10MB)超過を検知
| |
|— (3) 巨大なボディの残りを送信中 (TCPウィンドウ内) ——->| — (4) カーネルバッファを保護するため
| | 残りのデータを破棄(読み捨て)
| |
|<-- (5) HTTP/1.1 413 Payload Too Large --------------------|
|<-- (6) Connection: close ---------------------------------|
| |
|--- (7) TCP FIN (コネクション強制切断) ------------------->|
| |
1. クライアントが巨大な `Content-Length` を指定してリクエストを送信する。
2. リバースプロキシ(Nginxなど)やWebサーバーが、設定された許容サイズ(例: 10MB)を超えていることをヘッダー(または受信初期段階)で検知する。
3. サーバーは413レスポンスを返す準備をするが、TCPのストリーム上では、まだクライアントが残りの巨大なデータを送り続けている最中 である可能性がある。
4. サーバーは受信したデータをそのまま捨て(ドロップし)、クライアントに `413` と共に `Connection: close` を叩きつける。
5. サーバー側からTCPの `FIN` パケットを送り、強制的にソケットを閉じる。
もしここで `Connection: close` を返さずにKeep-Aliveを維持しようとすると、サーバーはクライアントが送り終えるまでの数ギガバイト(あるいは数十分)のパケットを延々と受信し続けなければならず、リソースが枯渇(Slowloris攻撃に似た状態)してしまう。`Connection: close` は、インフラを守るための防壁なのだ。
—
3. 実務で遭遇する設定ミスと各ミドルウェアのチューニング
現場で最も多いトラブルは、「APIの仕様書では20MBの画像アップロードを許可しているはずなのに、なぜか413エラーになる」というものだ。大抵の場合、これはWebアプリケーションフレームワークではなく、その手前にあるリバースプロキシやAPIゲートウェイの制限に引っかかっている。
ここでは、代表的なインフラストラクチャにおける設定変更の方法を見ていこう。
A. Nginx の場合 (`client_max_body_size`)
Nginxのデフォルトの制限値はわずか 1MB だ。モダンなWebアプリにおいて、1MBは少しリッチなJSONや画像ですぐに吹き飛んでしまう。
/etc/nginx/nginx.conf または 各バーチャルホストの設定ファイル
http {
# サーバー全体、またはバーチャルホスト、ロケーション単位で設定可能
# ここでは余裕を持って 20MB に設定
client_max_body_size 20M;
# 万が一無制限にしたい場合は 0 を指定(セキュリティリスクを考慮して非推奨)
# client_max_body_size 0;
}
※変更後は必ず `nginx -t` で構文チェックを行い、`nginx -s reload` を忘れないこと。
B. Apache httpd の場合 (`LimitRequestBody`)
Apacheの場合は、`LimitRequestBody` ディレクティブを使用する。
httpd.conf または .htaccess
制限値を 20MB (20971520 バイト) に設定。無制限にする場合は 0 を指定。
LimitRequestBody 20971520
C. Node.js (Express) の場合
インフラの手前を抜けてアプリケーションサーバーまで到達した場合でも、フレームワーク側でガードがかかっている。Express 4.16.0以降では、body-parserが標準統合されているため、以下のようにサイズを指定する。
const express = require(‘express’);
const app = express();
// JSONボディの制限を 20MB に拡張(デフォルトは 100kb!)
app.use(express.json({ limit: ’20mb’ }));
// URLエンコードされたデータも同様に拡張する場合
app.use(express.urlencoded({ limit: ’20mb’, extended: true }));
app.post(‘/api/upload’, (req, res) => {
res.json({ status: ‘success’, message: ‘データを受信しました’ });
});
app.listen(3000, () => {
console.log(‘Server is running on port 3000’);
});
—
4. デバッグとクライアント側の実装ハンドリング
インフラエンジニアとして、単に「設定を直しました」で終わらせては一流とは言えない。クライアント側(フロントエンドや他システム連携)のエンジニアが、413エラーに直面したときに適切なハンドリングができるよう、コード例を示しておく。
クライアント側の挙動(Fetch API の例)
413エラーを受け取った際、クライアント側は「リクエストが大きすぎること」をユーザーに伝えるか、分割送信(チャンク分割など)に切り替える必要がある。
async function uploadLargeData(payload) {
try {
const response = originFetch(‘/api/upload’, {
method: ‘POST’,
headers: {
‘Content-Type’: ‘application/json’
},
body: JSON.stringify(payload)
});
if (response.status === 413) {
console.error(‘エラー: 送信しようとしたデータが大きすぎます(Payload Too Large)。’);
// ここでユーザーへのアラート表示や、データ圧縮・分割処理を呼び出す
throw new Error(‘Payload Too Large (413)’);
}
if (!response.ok) {
throw new Error(`予期せぬエラーが発生しました: ${response.status}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error(‘通信エラー:’, error.message);
}
}
デバッグ時の `curl` コマンドTips
インフラの挙動を直接検証するには、適当なダミーデータを作って `curl` で叩くのが一番早い。
あえて巨大なデータを生成してリクエストを送信し、413とConnection: closeの挙動を確認する
-i オプションでレスポンスヘッダー(Connection: close等)を必ず確認すること
curl -i -X POST http://localhost/api/upload \
-H “Content-Type: application/json” \
-d “{\”data\”: \”$(printf ‘A%.0s’ {1..20000000})\”}”
このコマンドを実行した際、レスポンスヘッダーに以下が出力されていれば、意図した通りサーバーが防衛機制を発動している証拠だ。
HTTP/1.1 413 Payload Too Large
Server: nginx/1.18.0
Content-Type: text/html
Connection: close
—
まとめ:ネットワークの「境界線」を意識せよ
HTTP 413 (Payload Too Large) と `Connection: close` は、単なるエラーコードの組み合わせではない。それは、「予測不可能な巨大トラフィックから、背後のシステムリソースを如何にして守り抜くか」 という、ネットワーク設計の思想そのものだ。
Web APIを設計する際、あるいはインフラのサイジングを行う際は、常に以下の3点をチェックリストに加えてほしい。
1. クライアントからサーバーに至るまでの「すべての経路(CDN, WAF, ロードバランサー, リバースポキシ, アプリ)」にサイズ制限が存在する事実を忘れていないか?
2. 一番手前のレイヤーで適切なエラー(413)と切断(Connection: close)を行い、バックエンドの無駄な負荷を防いでいるか?
3. クライアント側が413を受け取った際に、優しくリカバリできるUX(または再送制御)が担保されているか?
バックエンドのコードを書くだけがエンジニアではない。パケットが流れる道筋のどこにダムがあり、どこに水門があるのかを把握してこそ、真に頑健なネットワークアーキテクチャが構築できるのだ。
さあ、コーヒーを飲み干したら、次はログの解析に戻ろうか。
コメント