【実務・中級編】HTTP/2におけるエラーコード(RST_STREAM)の分類と原因 – HTTPプロトコル・通信規格実践ガイド

HTTP/2の闇を暴く:RST_STREAMエラーコード全解釈と現場のトラブルシューティング

こんにちは。ネットワークの配管工から始まり、数々のWebシステムの修羅場をくぐり抜けてきたシニアエンジニアの私だ。

HTTP/2が世に出てからというもの、私たちは「1つのTCPコネクション上で複数のリクエスト・レスポンスを同時並行(マルチプレクシング)で流す」という、かつてのHTTP/1.1では夢物語だった高効率な世界を手に入れた。Head-of-Line Blocking(行頭ブロック)の呪縛から解放され、ブラウザもサーバーも軽快に動く……はずだった。

しかし、現場はいつも甘くない。
「なぜか特定のAPIリクエストだけが突然切断される」
「プロキシを挟んだ瞬間に原因不明の通信エラーが起きる」

こうしたトラブルの裏で、密かに、しかし確実に犯人としての役割を果たしているのが、今回解説する `RST_STREAM`フレーム だ。HTTP/1.1の「コネクション丸ごと切断(`Connection: close`)」とは異なり、HTTP/2は問題のある特定のストリームだけをピンポイントで死刑に処すことができる。それが `RST_STREAM` である。

今日は、この `RST_STREAM` が持つエラーコードの深層を覗き、現場で使える実践的なデバッグ手法を伝授しよう。

—

1. なぜ `RST_STREAM` が重要なのか?(HTTP/1.1との決定的な違い)

HTTP/1.1では、ひとたび通信の整合性が崩れたり、クライアントが途中でリクエストをやめたくなったりした場合、選択肢はほぼ一つしかなかった。「TCPコネクションそのものを切断(FIN/RST)し、3Wayハンドシェイクからやり直す」という荒業だ。

だが、HTTP/2は1本のTCPパイプラインの中に、複数の「ストリーム(仮想的な通信レーン)」を同居させている。もし1つの画像取得リクエストがおかしくなったからといって、同じコネクションで流れている重要なAPIレスポンスまで巻き添えにして切断するのは、リソースの無駄遣いであり、パフォーマンスの殺人行為だ。

そこで登場するのが `RST_STREAM` フレーム(Type: 0x3) である。

+————————————————-+
| Length (24) |
+—————–+—————+—————+
| Type (8) | Flags (8) |
+—————–+—————+—————–+
|R| Stream Identifier (31) |
+—————–+———————————+
| ErrorCode (32) |
+————————————————-+

ストリーム識別子を指定し、なぜそのストリームを一方的に強制終了(Abort)させたのかを 32ビットのエラーコード で相手に伝える。これがHTTP/2の美しくもシビアなエラーハンドリングの仕組みだ。

—

2. 主要なRST_STREAMエラーコードと発生条件

RFC 7540で定義されているエラーコードのうち、実務の現場で遭遇する確率が高い主要なものをピックアップした。それぞれの「真の発生原因」を理解しておこう。

| エラーコード名 | 値 (Hex) | 現場での主な発生条件と実態 |
| :— | :— | :— |
| NO_ERROR | `0x0` | 正常終了。クライアントが途中で不要になりキャンセルした場合など。 |
| PROTOCOL_ERROR | `0x1` | フレームのシーケンス違反、不正なフラグ、仕様違反のデータ構造。 |
| INTERNAL_ERROR | `0x2` | サーバー側の予期せぬバグ、例外発生、DB接続断など。 |
| CANCEL | `0x8` | クライアントがタイムアウトやユーザー操作でリクエストを破棄した。 |
| REFUSED_STREAM | `0x7` | サーバーが「処理を始める前に」リクエストを拒絶した(高負荷時など)。 |
| ENHANCE_YOUR_CALM| `0xb` | レートリミット(流量制限)超え。叩きすぎ。 |

① `PROTOCOL_ERROR` (0x1)

  • 意味: プロトコル違反。
  • 現場の背景: 大抵はクライアントとサーバーの「実装の解釈違い」や、間に挟まった悪しきプロキシ(Reverse Proxy / API Gateway)が不正なヘッダーやフレームを中継・改ざんした時に起きる。HTTP/2は仕様が厳格なため、小文字化されていないヘッダー名や、不正な擬似ヘッダー(`:method` や `:path` の順序ミスなど)を送りつけると、一発でこのエラーを食らう。

② `CANCEL` (0x8)

  • 意味: ストリームのキャンセル。
  • 現場の背景: Fetch APIなどでリクエストを投げた後、ユーザーが画面を遷移させたり、フロントエンド側で `AbortController` を使って明示的に通信を打ち切った場合に発生する。サーバー側から見れば「まだ処理の途中だったのに急にクライアントが消えた」状態になる。

③ `REFUSED_STREAM` (0x7)

  • 意味: 処理着手前の拒絶。
  • 現場の背景: サーバーが「今、手一杯なんだよ!」と悲鳴を上げている状態。或者は、冪等性(Idempotency)のないリクエストで、サーバーが安全のために処理を拒否した場合。このエラーコードが返ってきた場合、クライアント側でリトライ(再送)することが安全に許可されている(他のエラーコードではコネクション全体の破損を伴うため再送判断が難しい)。

—

3. 通信フロー:CANCEL と REFUSED_STREAM の動き

言葉だけではイメージしにくいので、ブラウザ/クライアントとHTTP/2サーバー間のシーケンスを見てみよう。

パターンA:クライアント都合のキャンセル (`CANCEL`)

[Client] [Server (Nginx / Envoyなど)]
| |
|— HEADERS (Stream ID: 1) ———————->| (処理開始)
| |
| (ユーザーが画面離脱 / AbortController発動) |
| |
|— RST_STREAM (Stream ID: 1, Error: CANCEL) —->|
| | (処理を中断しリソース解放)

サーバー側で無駄なDBクエリや重い処理を回し続けずに済む、HTTP/2の非常にスマートなリソース保護メカニズムだ。

—

4. 実務でのトラブルシューティング手法(デバッグの現場から)

では、実際に本番環境やステージングで `RST_STREAM` に遭遇した時、シニアはどうやって原因を特定するのか。その手順をステップ・バイ・ステップで解説する。

Step 1: パケット(生データ)をキャプチャする

ブラウザのDevTools(Networkタブ)では、HTTP/2のエラーは単に `net::ERR_HTTP2_PROTOCOL_ERROR` のような抽象的なメッセージとして片付けられてしまうことが多い。これではデバッグにならない。
必ず `tcpdump` や Wireshark を使って、TLSを復号しながらパケットを覗く。

tcpdumpでTLSセッションキーを環境変数に吐き出しつつキャプチャする例
export SSLKEYLOGFILE=~/sslkeylog.log
curl –http2 -v https://api.example.com/v1/data

Wiresharkで `http2.type == 3`(RST_STREAMフレーム)のフィルターをかけ、どのStream IDで、どのError Codeが返っているかを直接目視確認する。これが最速の真実だ。

Step 2: リバースプロキシ(Envoy / Nginx)のログを精査する

多くの場合、クライアントとバックエンドアプリの間にプロキシが存在する。プロキシがエラーを吐いているケースが大半だ。

例えば、Nginxのエラーログ(`error.log`)で以下のようなログが出ていないか確認する。

2023/10/25 10:00:00 [alert] 12345#12345: 6789 http2 process header error (129: HPACK index not found) while reading client request headers, client: 192.168.1.10, server: api.example.com

これはHPACK(ヘッダー圧縮)のコンテキストがズレたことによる `PROTOCOL_ERROR` だ。クライアントライブラリのバグや、途中のロードバランサーがHTTP/2のアップグレードに失敗している可能性が高い。

—

5. コードと設定の実装例

開発現場やインフラ設定で、エラーを防ぎ、適切にハンドリングするための実践コードを見ておこう。

① フロントエンド(JavaScript / Fetch API)でのキャンセル処理 (`CANCEL`)

`AbortController` を用いて、タイムアウトや不要になったリクエストを綺麗に切断する実装だ。これにより、サーバーへ無駄な `RST_STREAM (CANCEL)` を送り、リソースを適切に管理できる。

// タイムアウト付きの安全なAPIリクエスト関数
async function fetchUserData(userId) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000); // 3秒でタイムアウト

try {
const response = await fetch(`https://api.example.com/users/${userId}`, {
method: ‘GET’,
signal: controller.signal // AbortControllerを紐付け
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

return await response.json();

} catch (error) {
if (error.name === ‘AbortError’) {
console.warn(‘リクエストはタイムアウト、またはクライアント側でキャンセルされました(RST_STREAM: CANCEL送信)。’);
} else {
console.error(‘通信エラーが発生しました:’, error.message);
}
} finally {
clearTimeout(timeoutId);
}
}

② NginxにおけるHTTP/2バッファ・タイムアウトチューニング設定

インフラレイヤーで `RST_STREAM` が多発する場合、Nginxなどのリバースプロキシ側のバッファサイズやタイムアウト値がタイトすぎることが原因のことが多い。以下のパラメータをチューニングすることで、不要な切断を防げる。

server {
listen 443 ssl http2;
server_name api.example.com;

# SSL証明書の設定などは省略…

# HTTP/2関連のパフォーマンス・バッファチューニング
# クライアントからの大きなヘッダーやリクエストボディによるPROTOCOL_ERRORを防ぐ
http2_max_field_size 16k; # 単一ヘッダーの最大サイズ
http2_max_header_size 32k; # リクエストヘッダー全体の最大サイズ
http2_chunk_size 8k; # データ処理のチャンクサイズ

# ストリームの同時並行数の上限(過負荷によるREFUSED_STREAMを防ぐ、または適切に制御する)
http2_max_concurrent_streams 128;

location / {
proxy_pass http://backend_upstream;
proxy_http_version 1.1; # バックエンドへはHTTP/1.1で接続する場合の例

# タイムアウトを適切に持たせることで、不意のRST_STREAMを抑制
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
}

—

6. まとめ

HTTP/2の `RST_STREAM` は、一見すると「突然通信がプツッと切れる厄介なエラー」に思えるかもしれない。しかし、その内訳であるエラーコード(`PROTOCOL_ERROR`, `CANCEL`, `REFUSED_STREAM` など)は、ネットワークの状態やクライアント・サーバーの意図を正確に伝えるための極めて洗練されたシグナルである。

障害に直面したときは、ただ画面のエラーメッセージに絶望するのではなく、以下の手順を踏んでほしい。

1. Wiresharkやtcpdumpでパケットをキャプチャし、実際のRST_STREAMエラーコードを特定する。
2. エラーコードが `CANCEL` ならフロントエンドのライフサイクルやタイムアウトを見直す。
3. エラーコードが `PROTOCOL_ERROR` なら、プロキシやクライアントライブラリのHTTP/2実装・ヘッダー仕様を疑う。
4. エラーコードが `REFUSED_STREAM` なら、サーバーの負荷状況や同時接続数制限を確認し、安全なリトライ機構を設計する。

この視点を持っていれば、どんなに複雑なマイクロサービス間の通信トラブルであっても、パケットの軌跡をたどることで必ず真犯人に辿り着けるはずだ。

さあ、ログを開き、パケットを読もう。ネットワークは嘘をつかない。

コメント

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