【実務・中級編】HTTP/2におけるコネクションレベルのエラー(CONNECTION_ERROR) – HTTPプロトコル・通信規格実践ガイド

HTTP/2コネクションの「一蓮托生」:CONNECTION_ERRORが壊すもの、救うもの

Web APIの設計や大規模インフラの運用に携わっていると、一度は頭を悩ませるのがプロトコル層のトラブルだ。HTTP/1.1の時代であれば、keep-aliveなTCPコネクション上でリクエストが詰まっても、最悪の場合は当該リクエストを送り直すか、コネクションをサクッと張り直せば何とかなった。

しかし、HTTP/2の世界は違う。
1本のTCPコネクション上で複数のリクエストとレスポンスを同時に多重化(マルチプレクシング)するこのプロトコルは、非常にエレガントであると同時に、「一つの致命傷がすべてを巻き添えにする」という脆さを合わせ持っている。

今回は、その最たる例である「CONNECTION_ERROR(コネクションレベルのエラー)」に焦点を当てる。
RFC 7540が定めたこの冷徹な仕組みがなぜ存在するのか、どのような条件で発動し、現場のエンジニアとしてどう立ち回るべきなのか。パケットの息遣いを感じながら、深く掘り下げていこう。

—

1. HTTP/2の「生命線」を断つ:CONNECTION_ERRORとは何か

HTTP/2には、エラーの通知方法として大きく分けて2つのレベルが存在する。

1. ストリームエラー (STREAM_ERROR)
特定のストリーム(個別のリクエスト/レスポンス)だけが異常終了する。他のストリームは影響を受けないため、マルチプレクシングの恩恵を維持できる。
2. コネクションエラー (CONNECTION_ERROR)
TCPコネクション全体が即座に無効とみなされ、強制切断される。そのコネクション上で動いていたすべてのストリームが容赦なく巻き添え死する。

シニアエンジニアとして後輩によく言うのは、「CONNECTION_ERRORはプロトコル秩序に対する反逆罪である」ということだ。

HTTP/2のパーサーが「これはもう仕様として解釈できない」「セキュリティ上有害だ」と判断した瞬間、サーバーまたはクライアントは `RST_STREAM` ではなく、`GOAWAY` フレームをブチかまし、問答無用でTCPの四方向ハンドシェイク(あるいはRSTパケット)へと突入する。

なぜ、コネクションごと切断する必要があるのか?

最大の理由は「HPACKの状態同期(コンテキスト)の崩壊」にある。
HTTP/2はヘッダー圧縮にHPACKを採用しており、送信側と受信側で動的なヘッダーテーブルの状態を完全に一致させている。もし、パケットの構文ミスや不正なインデックス値を受信した際、その場しのぎで一部の処理を続行すると、双方のテーブルの整合性が完全に狂い、以後の通信がすべてデコード不能のゴミになってしまう。

だからこそ、「これ以上この土俵で会話を続けるのは危険だ」として、テーブルごとコネクションをリセットするしかないのだ。

—

2. どんな時に発動するのか?(主な発生トリガー)

実務で遭遇しがちなCONNECTION_ERRORの主なトリガーをいくつか挙げておこう。RFC 7540の仕様に裏打ちされた、代表的な「やってはいけないこと」たちだ。

  • 不正なフレームサイズ・タイプの受信

HTTP/2のフレームには厳格な長さ制限がある。設定された最大フレームサイズを超えるデータが送りつけられたり、定義されていない未知のフレームタイプが不適切な文脈で届いた場合。

  • HPACKデコードの失敗

動的テーブルのサイズ上限を超えたインデックスを指定したり、存在しない参照先を引いた場合。

  • SETTINGSフレームの不手際

接続開始直後のハンドシェイクにおいて、必須のSETTINGSフレームに対するACKが正しく返されない、あるいは不正なパラメーターが設定された場合。

  • ストリームIDの逆転・重複

クライアントが使うべきストリームID(奇数)をサーバーが使ったり、すでにクローズされたはずのストリームIDに対して新しいフレームが送られてきた場合。

—

3. 実際の通信フロー:GOAWAYから切断までの軌跡

トラブルシューティングでWiresharkやtcpdumpを開いたとき、CONNECTION_ERRORが発生した瞬間は、次のような美しい(そして残酷な)ドラマとして記録される。

[Client] [Server/Proxy]
│ │
│ ─── DATA / HEADERS (不正なフォーマット) ─────────────────> │
│ │
│ <── GOAWAY (Last-Stream-ID: 5, Error: PROTOCOL_ERROR) ───── │ │ │ │ ※ この瞬間、双方でコネクションが無効化される │ │ │ │ ─── FIN / ACK (または TCP RST) ───────────────────────────> │
▼ ▼

サーバーは `GOAWAY` フレームの中で、「ここまで(例:Stream ID 5)の処理は受け付けた(あるいは処理中だった)が、これ以上は無理だ」という意思表示と、エラーコード(例:`0x1 = PROTOCOL_ERROR`)を伝える。そして、猶予を与えることなくTCPコネクションを断ち切る。

—

4. デバッグと実装の現場:コードと設定から見る回避策

では、私たちは開発者・インフラエンジニアとして、このCONNECTION_ERRORにどう向き合うべきか。具体的なコードと設定の観点から見ていこう。

① Envoy ProxyやNginxでの設定・チューニング

リバースプロキシ(Envoyなど)を運用している場合、バックエンドのアプリケーションやクライアントからの不正なトラフィックによってCONNECTION_ERRORが多発することがある。ログを詳細に採取できるように設定しておくことが極めて重要だ。

以下は、EnvoyのグローバルなHTTP/2設定の例である。

EnvoyのHTTP/2コネクション管理・エラー検知に関する設定例
http_protocol_options:
max_stream_duration: 5s
コネクションレベルのバッファや制約を厳格に定義
stream_idle_timeout: 30s
header_table_size: 4096 # HPACKの動的テーブルサイズ(デフォルト4KB)
max_concurrent_streams: 100 # 1つのコネクションあたりの最大同時ストリーム数
initial_stream_window_size: 65535 # フローコントロールの初期ウィンドウサイズ
initial_connection_window_size: 1048576 # コネクション全体のウィンドウサイズ(1MB)

実務Tips: `header_table_size` やフローコントロールのウィンドウサイズをクライアント側・サーバー側で極端に変えていると、環境によってはネゴシエーションに失敗してCONNECTION_ERRORに直結することがある。特にマイクロサービス間の通信(gRPC含む)では、双方の設定値の整合性を必ず確認してほしい。

② Python (httpx) によるエラーハンドリングの実装

現代のモダンなHTTPクライアント(Pythonの `httpx` や Goの `net/http` など)はHTTP/2をネイティブサポートしている。しかし、サーバー側から突然の `GOAWAY` やコネクション切断(`RemoteProtocolError` 等)食らった際のリトライロジックをどう書くかが、アプリケーションの堅牢性を分ける。

import httpx
import time

def robust_http2_request(url: str, payload: dict):
# HTTP/2を有効にしたクライアントを作成
# (httpxはデフォルトでHTTP/2をサポート、http2=Trueを指定)
with httpx.Client(http2=True, timeout=10.0) as client:
max_retries = 3
for attempt in range(max_retries):
try:
print(f”[{attempt + 1回目の試行}] リクエスト送信中…”)
response = client.post(url, json=payload)

# ステータスコードに応じた例外スロー
response.raise_for_status()
return response.json()

except httpx.RemoteProtocolError as e:
# HTTP/2のプロトコル違反やCONNECTION_ERROR起因の切断をキャッチ
print(f”[警告] HTTP/2 プロトコルエラーを検知しました: {e}”)
if attempt == max_retries – 1:
raise
# バックオフ(待ち時間)を入れてからリトライ
time.sleep(2 attempt)

except httpx.RequestError as e:
# その他のネットワーク層のエラー
print(f”[エラー] ネットワーク接続に失敗しました: {e}”)
raise

実行例(※テスト用エンドポイント)
data = {“action”: “sync”}
result = robust_http2_request(“https://api.example.com/v1/resource”, data)

このコードのポイントは、単なる `TimeoutError` や `ConnectError` だけでなく、`httpx.RemoteProtocolError` を明示的に捉えている点だ。HTTP/2特有の切断が発生した際、即座に「あ、プロトコル層で弾かれたな」と察知し、適切なバックオフを入れて再接続を試みる設計が、プロダクション環境では不可欠となる。

—

5. シニアからの教訓:トラブルシューティングのチェックリスト

最後に、現場でCONNECTION_ERRORに遭遇したとき、あなたが迷わず原因に辿り着くためのチェックリストを授けよう。

1. プロキシやAPI Gatewayのログを見ろ
Nginx、Envoy、あるいはAWS ALBなどのアクセスログやエラーログに `http2_protocol_error` や `GOAWAY` の記録がないか。多くの場合、エラーの詳細な理由(例:`RST_STREAM` の理由コードや、フレームサイズの不正など)が記録されている。
2. パケットキャプチャ(Wireshark)を恐れるな
HTTP/2は暗号化(TLS)されているため、そのままでは読めない。しかし、ブラウザやクライアント側で `SSLKEYLOGFILE` 環境変数を設定し、Wiresharkに読み込ませれば、暗号化されたHTTP/2のフレーム(どのタイミングでどのIDの `GOAWAY` が飛んだか)が丸裸になる。これを使えるようになると、インフラエンジニアとしての戦闘力が一気に跳ね上がる。
3. 「ライブラリのバージョン」を疑え
自作のコードに問題がない場合、クライアント側またはサーバー側が使用しているHTTP/2ライブラリ(Goの `net/http`、Node.jsの `http2`、各種C/C++ライブラリなど)のバグを踏んでいる可能性がゼロではない。特にマイナーバージョンアップでHTTP/2のパーサー周りが修正されることはよくある。

HTTP/2のコネクションエラーは、一見すると冷酷で扱いにくいものに思えるかもしれない。しかし、それは「中途半端なデータでシステム全体の整合性を壊すくらいなら、一回綺麗にリセットして出直そうぜ」という、プロトコル設計者たちのいぶし銀の優しさでもあるのだ。

仕組みを正しく恐れ、パケットの声を聴きながら、堅牢なネットワークインフラを構築していこう。

コメント

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