HTTP/2の「暴力的な切断」の正体:RST_STREAMとエラーコードが語る通信の裏側
Web APIの設計や、大規模なインフラの運用現場に身を置いていると、時折不可解な通信エラーに直面する。HTTP/1.1の時代であれば、コネクションそのものがプツリと切断されるか、タイムアウトするかが分かりやすかった。しかし、1本のTCPコネクション上で複数のリクエストとレスポンスを同時に流すHTTP/2の世界では、話が少し厄介になる。
「コネクションは生きているのに、なぜか特定の通信だけが途中でプツンと消える」
この現象の裏で密かに、そして迅速に実行されているのが、今回焦点を当てる `RST_STREAM`フレーム だ。
教科書を開けば「ストリームを強制終了するためのフレームである」と一言で片付けられる。だが、現場のエンジニアにとって重要なのは、なぜそれが起きたのか、どのエラーコードが何を意味しているのか、そしてパケットキャプチャやログからどうやってその真相を暴くのかという「実践知」に他ならない。
今回は、シニアネットワークエンジニアの視点から、`RST_STREAM`の深層と、実務で役立つデバッグの技術を紐解いていこう。
—
1. なぜ `RST_STREAM` が必要なのか? HTTP/2の構造的背景
HTTP/2の最大の武器は「マルチプレクシング(多重化)」だ。1本のTCPコネクションの中に論理的な「ストリーム」を何本も生み出し、それぞれに一意なID(Stream ID)を割り振って、ヘッダーやデータを混在させて送受信する。
ここで想像してほしい。巨大な画像をダウンロードしている最中(Stream ID: 1)、ユーザーが別のページに遷移したため、そのダウンロードを今すぐ中止したくなったとする。
もしHTTP/1.1であれば、クライアントはTCPコネクション自体を切断するか、サーバー側がレスポンスを送り終えるのを黙って待つしかなかった。しかし、HTTP/1.1のためにわざわざコネクションを壊すのは、TCPの3ウェイハンドシェイクやスロースタートの恩恵を捨てることになり、ネットワーク資源の莫大な無駄遣いだ。
そこで登場するのが `RST_STREAM`(フレームタイプ: 0x3) である。
これは、「TCPコネクション全体を維持したまま、特定のストリームだけを即座に、かつ一方的に破棄・強制終了する」ための強力な制御信号だ。
[ TCP Connection ]
├── Stream ID: 1 (画像ダウンロード中) ──> [RST_STREAMで個別キャンセル]
├── Stream ID: 3 (APIリクエスト) ──> 正常に継続中
└── Stream ID: 5 (静的ファイルの取得) ──> 正常に継続中
この仕組みのおかげで、無駄な帯域幅の消費を防ぎ、サーバー側に対しても「もうそのデータはいらないからCPUやメモリのリソースを解放してくれ」とスマートに伝えることができる。
—
2. `RST_STREAM` フレームの構造と主なエラーコード
`RST_STREAM` フレームの構造は極めてシンプルだが、そのペイロードに含まれる 「32ビット(4バイト)のエラーコード」 が、トラブルシューティングにおける最大の手がかりとなる。
フレームの基本フォーマット
- Length: 4バイト(固定。エラーコードのサイズ)
- Type: 0x3 (`RST_STREAM`)
- Flags: 0x0(定義なし)
- Reserved + Stream Identifier: 4バイト(対象のストリームID)
- Payload: エラーコード (32-bit)
実務で頻出する主要なエラーコード一覧
RFC 7540で定義されているエラーコードのうち、現場で必ず押さえておくべき主要なものをいくつかピックアップしよう。ログやパケットアナライザでこれらのコードを見つけたら、それが原因の特定への最短ルートとなる。
| エラーコード名 | 値 (Hex) | 意味・実務上のコンテキスト |
| :— | :— | :— |
| NO_ERROR | `0x0` | 正常終了。キャンセルやリクエストの意図的な中止など、エラーではない理由でストリームを閉じる場合。 |
| PROTOCOL_ERROR | `0x1` | プロトコル違反。HTTP/2の仕様に反するフレーム構造や順序検知時に送出される。 |
| CANCEL | `0x8` | クライアント側がリクエストをもはや必要としなくなった場合(タイムアウト、ユーザーによるキャンセルなど)。 |
| REFUSED_STREAM | `0x7` | サーバーがリクエストを処理する前に拒否した場合。冪等性のあるリクエストであれば、別のコネクションで再送安全。 |
| INTERNAL_ERROR | `0x6` | サーバー内部で予期せぬエラー(例外発生など)が起きた場合。 |
| HTTP_1_1_REQUIRED | `0xd` | HTTP/2では処理できないため、HTTP/1.1へフォールバックを要求する場合。 |
特に `CANCEL (0x8)` と `REFUSED_STREAM (0x7)` は、Web APIのクライアント・サーバー間や、前段に配置されたリバースプロキシ(NginxやEnvoyなど)のログで非常によく目にする値だ。
—
3. 通信シーケンス:何が起きているのか?
実際にクライアントがリクエストを途中でキャンセルした際の、パケットレベルのやり取り(シーケンス)を見てみよう。
Client Server/Proxy
| |
|— HEADERS (Stream ID: 3) ————————>| (リクエスト送信開始)
|— DATA (Stream ID: 3) —————————>|
| |
| (ユーザーが操作を中断、またはタイムアウト発生) |
| |
|— RST_STREAM (Stream ID: 3, Error: CANCEL) ——>| (即座に破棄を要求)
| |
| | (サーバーは該当ストリームの
| | 処理とメモリを即座に解放)
| |
(TCPコネクション自体は切断されず、他のストリームは維持)
ポイントは、TCPのFINやRSTパケットとは全く別物だという点だ。あくまでHTTP/2レイヤー(TLSの上、あるいは平文のTCP上)のストリーム単位の制御であるため、このやり取りの後も同じTCPコネクション上で別のAPIリクエスト(例: Stream ID: 5)を何食わぬ顔で流し続けることができる。
—
4. 実務での実装例:コードと設定から挙動を理解する
理論を頭に入れたところで、実際にコードや設定ファイルでどのようにこの挙動に関わるかを見ていこう。
① Fetch API におけるタイムアウトとキャンセル (JavaScript)
モダンなWebフロントエンドやNode.js環境で、リクエストを途中で打ち切る際には `AbortController` を使用する。これが実行されると、内部的にはブラウザやランタイムがHTTP/2の `RST_STREAM (CANCEL)` をサーバーへ送出する。
// AbortControllerのインスタンスを生成
const controller = new AbortController();
const { signal } = controller;
// 3秒後にタイムアウトさせるタイマーを設定
const timeoutId = setTimeout(() => {
controller.abort(); // ここでHTTP/2のRST_STREAM(CANCEL)がトリガーされる
}, 3000);
try {
const response = await fetch(‘https://api.example.com/heavy-data’, {
signal: signal,
headers: {
‘Accept’: ‘application/json’
}
});
clearTimeout(timeoutId);
const data = await response.json();
console.log(‘データ取得成功:’, data);
} catch (error) {
if (error.name === ‘AbortError’) {
console.warn(‘リクエストはタイムアウト、またはユーザーによってキャンセルされました (RST_STREAM送信)’);
} else {
console.error(‘その他の通信エラー:’, error);
}
}
② Nginxリバースプロキシにおけるタイムアウト設定
インフラエンジニアとしてNginxを運用する場合、バックエンドの処理が遅延した際にNginx側から `RST_STREAM` を送出するケースがある。以下の設定は、バックエンドからの応答がない場合にプロキシ側からストリームを切り捨てる典型例だ。
server {
listen 443 ssl http2;
server_name api.example.com;
location / {
proxy_pass http://backend_cluster;
# バックエンドからのレスポンス待ちタイムアウト (デフォルト60秒)
proxy_read_timeout 10s;
# 送信タイムアウト
proxy_send_timeout 10s;
# もしこの時間を超えてもバックエンドが返答しない場合、
# Nginxはバックエンドとのストリームに対し RST_STREAM を送信し、
# クライアントには 504 Gateway Time-out を返す。
}
}
—
5. トラブルシューティング:現場で `RST_STREAM` に直面したときのデバッグ手順
本番環境で「なんだかAPIのレスポンスが途切れる」「一部のクライアントからエラー報告が上がる」というとき、ネットワークスペシャリストはどのように原因を特定すべきか。私流のステップを共有しよう。
ステップ1: パケットキャプチャ(Wireshark / nghttp2)で実体を暴く
ブラウザのDevToolsやアプリケーションログだけでは、エラーコードの全貌が見えないことがある。そんなときは `nghttp` コマンドやWiresharkで生のフレームを覗き見する。
Wiresharkであれば、フィルターに `http2.type == 3`(RST_STREAM)を指定する。
詳細ペインを展開し、「Error Code」 の数値を確認する。
- `0x8 (CANCEL)` が多発している場合:
- クライアント側のタイムアウト設定が短すぎないか?
- フロントエンドの画面遷移やリトライロジックが早すぎないか?
- `0x1 (PROTOCOL_ERROR)` が出ている場合:
- クライアント・サーバー間のHTTP/2実装(ライブラリのバージョンなど)に非互換性やバグがないか?
- `0x7 (REFUSED_STREAM)` が出ている場合:
- バックエンドサーバーやロードバランサーが過負荷でリクエストを拒絶(Shedding)していないか?
ステップ2: プロキシやロードバランサーのログ出力をカスタマイズする
EnvoyやNginx、あるいはAWSのALBなどを使用している場合、デフォルトのアクセスログにはHTTP/2の詳細なエラーコードが出力されないことが多い。
例えばNginxであれば、ログフォーマットに `$http2_error_code` や関連する変数を組み込み、どのストリームIDでどのようなエラーが頻発しているのかを可視化できるようにチューニングしておくことが、インフラエンジニアの必須スキルとなる。
—
まとめ
`RST_STREAM` フレームは、HTTP/2という高速道路において、事故を起こした車両を速やかに路肩へ排除し、全体の渋滞を防ぐための極めて洗練された仕組みだ。
一見すると「突然通信が切れた不具合」に見える現象も、その背後にあるエラーコードの意図を正確に読み解くことができれば、問題の本質(クライアントのタイムアウトなのか、プロキシの過負荷なのか、プロコル違反なのか)が手に取るようにわかるようになる。
フレームが語るシグナルに耳を澄まし、パケットの囁きを聞き分けること。それこそが、現代のネットワークスペシャリストやWebエンジニアに求められる、真のプロフェッショナルなスキルなのだ。
コメント