【実務・中級編】HTTP/2とHTTP/1.1のプロトコル変換(プロキシの挙動) – HTTPプロトコル・通信規格実践ガイド

HTTP/2とHTTP/1.1の「通訳」はなぜ泥臭いのか?:プロキシ境界におけるヘッダー変換とストリーム管理の罠

こんにちは。ネットワークスペシャリストの私です。

Webアプリケーションの高速化、そしてブラウザの進化に伴い、現代のインフラストラクチャはHTTP/2(さらにはHTTP/3)への移行を進めてきました。バックエンドのマイクロサービス群も、gRPCやHTTP/2ベースの通信を前提に設計されることが増えています。

しかし、現実のインターネットや企業内ネットワークは、一筋縄ではいきません。「クライアント側はレガシーなHTTP/1.1のままで、フロントエンドのリバースプロキシ(NginxやEnvoyなど)がHTTP/2にアップグレードし、バックエンドへは再びHTTP/1.1で流す」といった、新旧プロトコルの混在環境は今なお日常茶飯事です。

この「プロトコル変換(Protocol Translation)」、文字にするとサラッとしていますが、現場のインフラエンジニアやWeb APIデザイナーにとっては悪夢のようなデバッグ地獄の入口になり得ます。

今回は、HTTP/2の美しい多重化・圧縮の世界と、HTTP/1.1のテキストベースの世界をブリッジするリバースプロキシの裏側で、何が起きているのか。その技術的課題と実務での対策を、実例を交えて紐解いていきましょう。

—

1. なぜプロトコル変換が必要なのか?そして何が面倒なのか

HTTP/2は、単一のTCPコネクション上に「ストリーム」という仮想的な通信路を多重化し、バイナリフレームで効率よくデータをやり取りします。さらに、HTTPヘッダーはHPACKという強力なアルゴリズムで動的に圧縮されます。

一方、HTTP/1.1はテキストベースのプロトコルであり、Keep-Aliveを使っていても、リクエストとレスポンスは基本的に直列(Head-of-Line Blockingの影響を受ける)で流れます。

この両者を仲介するリバースプロキシ(Nginx、Envoy、API Gatewayなど)は、次のような「通訳」を毎秒何万回も行っています。

1. HTTP/2クライアントからの受信: バイナリフレームをデコードし、HPACKを展開してヘッダーを復元し、ストリーム単位でリクエストを解釈する。
2. HTTP/1.1バックエンドへの転送: 復元したリクエストを再びテキスト形式のHTTP/1.1にシリアライズし、TCPソケット経由でバックエンドへ流す。
3. レスポンスの逆変換: バックエンドからのHTTP/1.1レスポンスを読み込み、HTTP/2のバイナリフレームとHPACKエンコードに変換してクライアントへ返す。

この翻訳作業のどこに落とし穴があるのでしょうか? 主な原因は「表現力の非対称性」にあります。HTTP/2だけに存在する概念(疑似ヘッダー、ストリーム優先度など)を、HTTP/1.1にどうマッピングするか、あるいはその逆の苦悩です。

—

2. ヘッダーマッピングの泥臭い現実:偽装される世界

HTTP/2では、リクエストの宛先やメソッドは `:method` や `:path` といった「疑似ヘッダーフィールド(Pseudo-Header Fields)」としてバイナリ化され、通常のヘッダーとは厳密に区別されます。

これに対し、HTTP/1.1にはそんな概念はありません。最初の行(リクエストライン)に `GET /index.html HTTP/1.1` のようにベタ書きします。

プロキシが行うヘッダー変換の裏側

リバースプロキシは、HTTP/2のストリームから以下のようにデータを再構築します。

  • `:method` (例: `GET`) + `:path` (例: `/api/v1/users?id=10`) + `:authority` (例: `api.example.com`)

↓ 変換

  • `GET /api/v1/users?id=10 HTTP/1.1`
  • `Host: api.example.com`

ここで問題になるのが、「HTTP/2特有のヘッダーのハンドリング」です。

1. 大文字・小文字の厳格さ: HTTP/1.1ではヘッダー名の大文字小文字は区別されません(例: `Content-Type` でも `content-type` でも動く)。しかし、HTTP/2のHPACK仕様では、すべてのヘッダー名は小文字でなければならないと規定されています。もし古いHTTP/1.1アプリケーションが `X-Custom-Header` を厳密に大文字混じりで期待している場合、プロキシが小文字(`x-custom-header`)に正規化してしまい、バックエンドでアプリケーションエラーが発生するトラブルが後を絶ちません。
2. コネクション管理ヘッダーの排除: HTTP/2では、`Connection`, `Keep-Alive`, `Proxy-Connection`, `Transfer-Encoding`, `Upgrade` といったコネクション単位の制御ヘッダーの使用が厳禁(プロトコル違反)とされています。もしHTTP/2クライアント側(あるいはバックエンド)がこれらを送出してきた場合、プロキシは即座にこれらを削除または無効化しなければなりません。

—

3. ストリーム管理とフローコントロールの乖離

HTTP/2の最大の武器は「マルチプレクシング」です。1本のTCPコネクション上で、ストリームID(奇数はクライアント発、偶数はサーバー発)を使って数千のリクエストを同時に流せます。

しかし、バックエンドがHTTP/1.1である場合、プロキシはこのリクエストをどう裁く必要があるでしょうか?

[HTTP/2 Client] –(単一TCP / 多重ストリーム)–> [Reverse Proxy] –(複数のTCPコネクション or 順次送信)–> [HTTP/1.1 Backend]

コネクションプールの限界とHead-of-Line Blockingの再来

プロキシは、バックエンドのHTTP/1.1サーバーに対して複数のTCPコネクションを張り、いわゆる「コネクションプール」でリクエストを分散させます。

ここで発生するのが、「フロントエンド側はHTTP/2でパラレルにリクエストが来ているのに、バックエンドへのコネクション数が枯渇して詰まる」という現象です。
HTTP/2側ではストリームが流れてきているのに、バックエンドのHTTP/1.1の応答待ち(あるいはコネクションプールの最大数制限)に阻まれ、プロキシ内部のバッファが圧迫されます。

さらに厄介なのがフローコントロール(Window Update)の不整合です。
HTTP/2には、TCPとは別個に、ストリーム単位およびコネクション単位のフローコントロール(流量制御)が備わっています。クライアントが「今はこれ以上データを受け取れない」とウィンドウサイズを絞っているとき、バックエンドのHTTP/1.1が高速にデータを送り続けてくると、プロキシはメモリ上にそのデータを一時バッファリングし続けなければならず、メモリリークやOOM(Out of Memory)キラーの餌食になります。

—

4. 実務でのトラブルシューティング:Nginxでの設定と検証

言葉で説明するよりも、実際にインフラエンジニアが直面する設定とデバッグ手法を見てみましょう。ここでは、代表的なリバースプロキシである Nginx を例に取ります。

Nginxは、クライアント側とHTTP/2で通信し、バックエンドへはHTTP/1.1でプロキシするのが非常に得意ですが、デフォルトの挙動でハマるポイントがあります。

Nginx設定例 (`nginx.conf`)

以下の設定は、HTTP/2クライアントからのリクエストを受け付け、バックエンドのHTTP/1.1アプリサーバーへ適切にヘッダーやプロトコルをマッピングする実用的な設定です。

http {
# HTTP/2のHPACKバッファサイズやストリーム数のチューニング
http2_max_field_size 16k; # ヘッダーフィールドの最大サイズ
http2_max_header_size 32k; # ヘッダー全体の最大サイズ
http2_chunk_size 8k; # バイナリフレームのチャンクサイズ

upstream backend_app {
server 127.0.0.1:8080;
# バックエンドへのHTTP/1.1コネクションを維持するプール設定
keepalive 32;
}

server {
listen 443 ssl http2; # HTTP/2を有効化
server_name api.example.com;

ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;

location / {
proxy_pass http://backend_app;

# — HTTP/1.1バックエンドのためのプロキシ基本設定 —
proxy_http_version 1.1; # バックエンドへは確実にHTTP/1.1を使う

# コネクション維持のためのヘッダー上書き
proxy_set_header Connection “”;

# クライアントからのリアルIPやホスト名の継承
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# HTTP/2で問題になりやすいバッファリングの調整
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 8k;
}
}
}

チューニングのポイント

  • `proxy_http_version 1.1;` と `proxy_set_header Connection “”;` のセットは、Nginxがバックエンドへ接続する際に `Connection: close` が勝手に付与されるのを防ぎ、HTTP/1.1のKeep-Aliveを維持するために必須の呪文です。これが抜けると、リクエストごとにTCPの3ウェイハンドシェイクが発生し、パフォーマンスが劇的に悪化します。

—

5. デバッグ実践:パケットとログの覗き見方

「APIのレスポンスがたまに途切れる」「ヘッダーに予期せぬ値が入る」といったトラブルに直面したとき、プロキシの境界で何が起きているかを特定するための実務的なデバッグ手順を解説します。

ステップ1: Nginxのログで変換前後を追跡する

まず、Nginxのエラーログとアクセスログを詳細化します。特にHTTP/2のストリームIDや通信エラーを捉えるために、ログフォーマットに変数を含めましょう。

log_format h2_debug ‘$remote_addr – $remote_user [$time_local] ‘
‘”$request” $status $body_bytes_sent ‘
‘”$http_referer” “$http_user_agent” ‘
‘rt=$request_time uct=”$upstream_connect_time” uht=”$upstream_header_time” urt=”$upstream_response_time” ‘
‘hp2_stream=$http2_stream_id’; # HTTP/2ストリームIDを記録(環境による)

access_log /var/log/nginx/h2_debug.log h2_debug;
error_log /var/log/nginx/error.log debug; # デバッグレベルに引き上げ

ステップ2: `curl` を使ってHTTP/2で強制リクエストを飛ばす

クライアント側から意図したHTTP/2リクエストがプロキシにどう届いているかを確認するため、`curl` コマンドでHTTP/2を強制し、詳細な通信フロー(フレームのやり取り)を覗き見ます。

–http2 オプションを明示し、-v (verbose) でフレームやヘッダーのやり取りを出力する
curl -v –http2 https://api.example.com/v1/users \
-H “X-Custom-Test-Header: HelloHTTP2”

出力されるデバッグログの中で、以下のようなやり取りを確認します。

  • `Using HTTP/2, server supports multiplexing` が表示されているか。
  • 送信されたヘッダーが `:method: GET`, `:path: /v1/users`, `:authority: api.example.com` のように疑似ヘッダーに正しく分解されているか。

ステップ3: Wireshark / tcpdump による暗号化パケットの解析

TLS 1.3環境下であっても、デバッグ用のクライアント端末やプロキシサーバー上で秘密鍵(SSLKEYLOGFILE)を環境変数に設定すれば、WiresharkでHTTP/2のバイナリフレーム(HEADERSフレーム、DATAフレーム、RST_STREAMフレームなど)を直接目視できます。

特に、バックエンドとの接続エラー(`RST_STREAM` が頻発しているなど)がある場合、プロキシがどのタイミングで音を上げているのかが手に取るように分かります。

—

まとめ:境界を制する者がインフラを制す

HTTP/2とHTTP/1.1のプロトコル変換は、一見すると「プロキシが勝手によしなにしてくれる機能」に思われがちです。しかし、その裏側では、バイナリとテキスト、多重化と直列化、小文字強制と大文字許容といった、異なる設計思想のぶつかり合いが起きています。

トラブルシューティングの基本は、「今、パケットはどのレイヤーで、どのプロトコルとして解釈されているのか」を頭の中で常にシミュレーションすることです。

  • クライアント ⇔ プロキシ 間は HTTP/2(バイナリ・HPACK・マルチプレクシング)
  • プロキシ ⇔ バックエンド 間は HTTP/1.1(テキスト・Keep-Alive・コネクションプール)

この境界線で何が変換され、何が削ぎ落とされているのかを理解していれば、どんな難解なAPIの不具合やパフォーマンス劣化に直面しても、必ず原因にたどり着くことができます。

現場のインフラを守るエンジニアの皆さん、日々の泥臭いデバッグ作業、一緒に乗り越えていきましょう!

コメント

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