「Expect: 100-continue」の深淵:巨大なペイロードを投げる前に、我々が知るべきプロトコルの作法
ネットワークの最前線で戦うエンジニアなら、一度は「巨大なリクエストボディの送信」に頭を抱えたことがあるはずだ。クライアントが数GBのバイナリをアップロードしようとした矢先、サーバー側で認証エラーや容量超過が発覚し、貴重な帯域とRTTが霧散する。
HTTP/1.1の標準仕様(RFC 7231)にひっそりと存在する`Expect: 100-continue`は、この悲劇を防ぐための「同意を求める握手」である。しかし、このプロトコル上の礼儀作法は、現代の複雑なインフラスタックにおいて、時としてパフォーマンスの劇薬となる。今日は、このハンドシェイクの裏側をパケットレベルで解剖してみよう。
—
1. 100-continueのパケット挙動:期待と現実
クライアントが`Expect: 100-continue`ヘッダーを付けてリクエストを送信する際、それは「まだボディは送らない。サーバーが受け入れる準備ができているか確認したい」という信号だ。
通信フローの理想
1. Client -> Server: Headerのみ(`Expect: 100-continue`含む)を送信。
2. Server -> Client: サーバーが要件を満たせば `100 Continue` を即座に返す。
3. Client -> Server: 待機していたリクエストボディを流し込む。
ここで重要なのは、「クライアントはサーバーからのレスポンスを待つ」という点だ。もしサーバーの反応が遅ければ、クライアントは数ミリ秒〜数百ミリ秒の「無駄な待機時間」を強いられる。
実務上の罠:タイムアウトの設計
多くのクライアントライブラリ(cURLや各種言語のHTTPクライアント)には、`Expect`ヘッダーのレスポンスを待つための専用タイムアウト設定が存在する。例えば、cURLのデフォルト設定はしばしば「即時」または「短い閾値」に設定されているが、サーバー側で重い認証処理(JWTの検証やDBクエリ)が走っている場合、このタイムアウトが先に発火し、本来成功するはずだったアップロードが失敗するケースが多発する。
—
2. インフラ・トランスポート層からの最適化
`Expect: 100-continue`を有効活用しつつ、パフォーマンスを最大化するには、トランスポート層のチューニングが不可欠だ。
TCPバッファとウィンドウサイズ
サーバーが `100 Continue` を送るタイミングは、アプリケーション層の処理に依存する。もしサーバーがレスポンスを返す前にTCPの受信バッファが溢れれば、TCPウィンドウサイズはゼロになり、通信はスタックする。
Linuxカーネルで大容量のアップロードを捌くなら、`net.ipv4.tcp_rmem`や`net.core.rmem_max`を適切に広げ、高トラフィック環境では `BBR` 混雑制御アルゴリズムの適用を検討すべきだ。
TCPバッファの動的調整を最適化し、スループットを最大化する例
sysctl -w net.ipv4.tcp_rmem=”4096 87380 16777216″
sysctl -w net.ipv4.tcp_congestion_control=bbr
TLSハンドシェイクとの共存
TLS 1.3が普及した現在、`100-continue`の握手はTLSの暗号化トンネル内で完結する。特筆すべきは、Early Data (0-RTT)との相性だ。0-RTTと`Expect: 100-continue`を併用する場合、リプレイ攻撃のリスクが跳ね上がる。セキュリティ専門家として忠告するが、状態を変更するリクエスト(POST等)に0-RTTを安易に許可してはならない。
—
3. 実践:Nginxでのハンドリングとタイムアウト制御
現場で最も多いトラブルは「サーバーの反応待ちによるレスポンス遅延」だ。Nginxをリバースプロキシとして運用する場合、`client_body_buffer_size`と`proxy_read_timeout`のバランスを調整する必要がある。
Nginxの設定例
server {
# ボディをメモリに展開するサイズを調整(大きすぎるとメモリ圧迫、小さすぎるとディスクI/O発生)
client_body_buffer_size 128k;
# 100-continueのレスポンス自体に時間がかかる場合のタイムアウト設定
# アプリケーションサーバーのバックエンド応答速度に合わせて調整せよ
proxy_read_timeout 60s;
# 巨大なリクエストボディの制限
client_max_body_size 1G;
}
—
4. セキュリティ:Dos攻撃の踏み台にさせない
`Expect: 100-continue`を悪用したDos攻撃があることを忘れてはならない。攻撃者が「ヘッダーだけを大量に送りつけ、ボディを送らずに接続を維持する」ことで、サーバーの同時接続数(`worker_connections`)を枯渇させる手法だ。
この防御策として、以下の対策を徹底してほしい:
- `client_header_timeout`の適正化: リクエストヘッダーがすべて届くまでの時間を短くする(例: 10秒〜20秒)。
- `keepalive_timeout`の厳格化: 接続のライフサイクルを短く抑え、ゾンビコネクションを排除する。
—
結びに:プロトコルは「会話」である
`Expect: 100-continue`は、単なる仕様上のフラグではない。それは、クライアントとサーバーが「これから大きな荷物を運ぶが、準備はいいか?」という確認を行う、ネットワーク上の会話そのものだ。
この会話をいかにスムーズに行い、いかに早く「はい、受け入れます」と言えるか。それがインフラアーキテクトの腕の見せ所だ。パケットがどう流れ、どこで待たされ、カーネルがどうメモリを割り当てているか。その一挙手一投足に意識を向けることで、あなたのシステムはより堅牢で、より速いものへと進化するだろう。
さあ、次のパケットキャプチャで、この握手が正しく行われているかを確認してみてほしい。そこには、教科書には書かれていない「現場の鼓動」が流れているはずだ。
コメント