HTTP/2エラーの深層:パケットの裏側で何が起きているのか?
ネットワークエンジニアとして現場を渡り歩いていると、HTTP/1.1の「1つのTCPコネクションにつき1リクエスト」という世界がいかに牧歌的だったかを思い知らされます。TCPの頭塞ぎ(Head-of-Line Blocking)を打破するために登場したHTTP/2は、1本のTCPコネクション上で複数の「ストリーム」を同時に多重化(マルチプレクシング)させ、バイナリフレーミングとHPACKによるヘッダー圧縮で限界までスループットを絞り出します。
しかし、この高度な効率化の代償として、「一つのコネクション、あるいは特定のストリームで何かが狂ったとき、どうやってそれを正確に伝えるか」という問題が生じました。HTTP/1.1であれば「あ、なんか接続切れたな」で済んでいた話が、HTTP/2では細かく制御されるようになっています。
今回は、API設計やインフラ運用で必ず直面する「HTTP/2エラーコード」に焦点を当て、パケットの往来の裏側で何が起きているのか、そして現場でどうハンドリングすべきかを、シニアエンジニアの視点でお伝えします。
—
1. HTTP/2のエラー制御メカニズム:`RST_STREAM` と `GOAWAY`
HTTP/2では、エラーの伝播方法として大きく分けて2つのアプローチが用意されています。ここを取り違えると、トラブルシューティングの初手で盛大に迷子になります。
ストリーム単位の断絶:`RST_STREAM`
ある特定のストリーム(例えば、巨大な画像をダウンロードしている最中のストリーム)だけでタイムアウトや不正なデータ検知が起きた場合、コネクション全体を切断するのはあまりにも非効率です。ここで使われるのが `RST_STREAM`フレーム です。
- 影響範囲: 該当するストリームのみ。
- 挙動: クライアント・サーバー双方が、他のストリームの処理を継続したまま、問題のストリームを即座にアボート(破棄)できます。
コネクション全体の終焉:`GOAWAY`
プロトコルの違反、セキュリティ上の脅威、あるいはグレースフル・シャットダウンの際、HTTP/2セッションそのものを閉じたい場合に送信されるのが `GOAWAY`フレーム です。
- 影響範囲: コネクション全体。
- 挙動: 「これ以降の新規ストリームの作成は受け付けないが、すでに受け付けたストリームの処理は最後までやりきる」という優しさ(あるいは冷徹さ)を持った切断信号です。
—
2. 実務で遭遇する主要なエラーコード群
RFC 7540で定義されているエラーコードの中で、インフラ運用やAPI開発において特に頻出するものをピックアップします。それぞれの「意味」と「現場での臭い(原因)」を体に叩き込みましょう。
| エラーコード名 | 値 (Hex) | 意味・定義 | 現場での主な発生原因 |
| :— | :— | :— | :— |
| `NO_ERROR` | `0x0` | 正常終了。グレースフルな切断。 | サーバーの負荷分散やアイドルタイムアウトによる正常なセッション終了。 |
| `PROTOCOL_ERROR` | `0x1` | プロトコル違反の検出。 | 不正なフレーム構造、パディング違反、状態遷移の無視。リバースプロキシのバグなど。 |
| `INTERNAL_ERROR` | `0x2` | サーバー側の予期せぬ内部エラー。 | バックエンドアプリのクラッシュ、データベース接続断、パニック。 |
| `FLOW_CONTROL_ERROR`| `0x3` | フロー制御ウインドウのオーバーフロー。 | クライアント/サーバー間でウインドウサイズの計算が破綻した。 |
| `SETTINGS_TIMEOUT` | `0x4` | SETTINGSフレームに対するACKが返ってこない。 | ネットワークの深刻な遅延、または相手方のフリーズ。 |
| `CANCEL` | `0x5` | ストリームが不要になった。 | クライアント側のタイムアウト、ユーザーによるリクエストの手動キャンセル。 |
| `REFUSED_STREAM` | `0x7` | ストリームを開始できなかった。 | サーバーがまだリクエストを処理する準備ができておらず、安全にリトライ可能な状態。 |
| `ENHANCE_YOUR_CALM`| `0xb` | レートリミット違反、負荷過剰。 | 短時間での過剰なリクエスト、DDoS防御機構の発動。 |
—
3. 通信フロー(シーケンス)で見るエラーの舞台裏
文字情報だけではピンとこない方向けに、`RST_STREAM` と `GOAWAY` が流れる際のパケットのやり取りをシーケンスとして整理しておきましょう。
パターンA:特定のストリームでエラー(`RST_STREAM`)
Client Server
| —– HEADERS (Stream 3: GET /api/v1/heavy) ——> |
| | (内部処理でエラー発生!)
| <---- RST_STREAM (Stream 3, Error: INTERNAL_ERROR) - |
| |
| ----- HEADERS (Stream 5: GET /api/v1/light) ------> | (Stream 5は生きている!)
| <---- HEADERS & DATA (Stream 5) ------------------- |
このように、Stream 3がクラッシュして `RST_STREAM` が返されても、同一コネクション上のStream 5は何事もなかったかのように処理を続行できます。これがHTTP/2の真骨頂です。
パターンB:コネクション切断(`GOAWAY`)
Client Server
| | (シャットダウン開始)
| <---- GOAWAY (LastStream: 3, Error: NO_ERROR) ------ |
| |
| ----- HEADERS (Stream 5: 新規リクエスト) ----------> |
| <---- (サーバーはStream 5を無視/拒否) ------------- |
サーバーが `GOAWAY` を送信した時点で、すでに処理中のストリーム(LastStream ID以下)は処理されますが、クライアントが新しく発行したストリーム(Stream 5など)は拒絶されます。クライアントはこのシグナルを受け取ったら、新しいTCPコネクションを張り直す必要があります。
---
4. 実務でのデバッグとハンドリング実践
ここからは、実際にコードや設定を通じて、これらのエラーをどう検知し、どうハンドリングすべきかを見ていきましょう。
Python (httpx) によるエラーハンドリング
モダンなHTTP/2クライアントライブラリである `httpx` を使うと、HTTP/2特有のストリーム切断やプロトコルエラーをキャッチして適切にリトライを実装できます。
import httpx
import time
def fetch_with_http2_fallback(url: str):
# HTTP/2を有効にしたクライアントを作成
with httpx.Client(http2=True) as client:
max_retries = 3
for attempt in range(max_retries):
try:
print(f”[{attempt + 1}回目の試行] リクエスト送信: {url}”)
response = client.get(url, timeout=5.0)
# ステータスコードに応じた処理
response.raise_for_status()
return response.json()
except httpx.RemoteProtocolError as e:
# HTTP/2のPROTOCOL_ERRORやGOAWAYなどに起因するプロトコル違反の捕捉
print(f”[警告] HTTP/2プロトコルエラーを検出しました: {e}”)
if attempt == max_retries – 1:
raise
time.sleep(1.0)
except httpx.StreamError as e:
# 特定のRST_STREAMなどによるストリーム破棄の捕捉
print(f”[警告] HTTP/2ストリームエラー (RST_STREAM等): {e}”)
# REFUSED_STREAM等の場合は即座に再試行が安全
time.sleep(0.5)
except httpx.RequestError as e:
print(f”[エラー] ネットワーク層のエラー: {e}”)
raise
実行例(※テスト用URLに置き換えてください)
data = fetch_with_http2_fallback(“https://httpbin.org/json”)
Nginx リバースプロキシでのHTTP/2エラー対策設定
インフラ側(Nginx)でHTTP/2を運用する場合、クライアントからの不正なリクエストや、バックエンド(アップストリーム)との通信不良が原因で `PROTOCOL_ERROR` や `INTERNAL_ERROR` が発生することがあります。タイムアウトやバッファサイズを適切にチューニングし、不要なコネクション切断を防ぐ設定例です。
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# HTTP/2のストリームあたりの最大同時リクエスト数を制限
# 過剰な多重化によるリソース枯渇(PROTOCOL_ERRORの誘発)を防ぐ
http2_max_concurrent_streams 128;
# HTTP/2の受信バッファサイズの設定
http2_recv_buffer_size 256k;
location / {
proxy_pass http://backend_cluster;
proxy_http_version 1.1; # アップストリームへはHTTP/1.1で接続するのが一般的
# バックエンドからの応答待ちタイムアウトを適切に設定
# これが短すぎると、サーバー側でINTERNAL_ERRORやRST_STREAMにつながる
proxy_read_timeout 60s;
proxy_send_timeout 60s;
# クライアントからのキャンセル(CANCEL)をバックエンドにも伝える
proxy_ignore_client_abort off;
}
}
curl でのHTTP/2パケット挙動の確認
手元の環境で「今、サーバーはどんなエラーコードを返しているのか?」を確かめるには、`curl` の詳細なトレース機能が役立ちます。`-v` オプションに加え、HTTP/2のフレームを追うためにフレーム出力を有効にします(対応ビルドが必要ですが、パケットキャプチャと組み合わせるのが確実です)。
HTTP/2でリクエストを送り、詳細なネゴシエーションとフレームを確認
curl -v –http2 https://api.example.com/health
もしWiresharkでパケットをキャプチャする場合は、TLSの復号鍵(`SSLKEYLOGFILE` 環境変数などを使用)を設定した上で、フィルターに `http2.type == 3`(RST_STREAM)や `http2.type == 7`(GOAWAY)を指定してください。どのストリームIDでどのエラーコード(例: `0x2` = `INTERNAL_ERROR`)が飛んでいるかが一目瞭然になります。
—
5. シニアエンジニアからの実務的Tips
最後に、現場で数々の障害を踏んできた私から、HTTP/2エラーに直面したときの心得をいくつか授けます。
1. 「自動リトライ」の無限ループに気をつけろ
`REFUSED_STREAM` は「安全にリトライしてよい」と規格で定められていますが、サーバーが根本的に高負荷(`ENHANCE_YOUR_CALM`)に陥っているときにクライアントが一斉にリトライをかけると、リトライストーム(Thundering Herd Problem)を引き起こし、サーバーを完全に沈黙させます。指数バックオフとジッター(ランダムな揺らぎ)を必ずリトライロジックに組み込んでください。
2. HTTP/1.1フォールバックを恐れるな
どうしてもクライアントライブラリとロードバランサーの相性問題(変な独自パッチやプロキシのバグ)でHTTP/2の `PROTOCOL_ERROR` が解決しない場合、一時的にクライアント側で `http2=False` にしてHTTP/1.1へフォールバックさせるのは、泥臭いですが極めて確実な回避策です。可用性を最優先する現場の判断として持っておくべきカードです。
3. ログにはストリームIDを記録させろ
マイクロサービスアーキテクチャにおいて、API Gatewayやリバースプロキシのアクセスログには、必ず「HTTP/2のストリームID」や「コネクションID」を出力できるようにカスタムフォーマットを組んでおきましょう。「どのリクエストがどのストリームで死んだのか」が追えないログは、夜間障害のときにただの雑音になります。
HTTP/2は複雑ですが、そのエラーコードの背景にある仕様を正しく理解すれば、パケットの挙動が手に取るように分かるようになります。日々のインフラ運用やAPI設計の品質向上に、ぜひこの知見を役立ててください。
コメント