WebSocketのクローズコード1006は誰が送ったのか:Closeフレームと1000・1001・1011、送ってはいけないコード
WebSocketのログに出る1006は相手が送った値ではなく、Closeフレームを受け取らないまま接続が終わったことを示す。RFC 6455のクロージングハンドシェイク、コードの決まり方、コードの範囲、再接続のバックオフをまとめた。
WebSocketの接続が切れたあと、ログに 1006 が残っていると、サーバーが1006というコードを送って切断したと読みたくなる。しかしRFC 6455では、1006はどちらの側もフレームに入れて送ってはいけない値だ。接続が終わったのにCloseフレームを一度も受け取っていない、という状況に番号を付けるために空けてある枠である。
この記事はRFC 6455(2011年12月)の5.5.1節と7章、IANAのWebSocket Close Code Number Registry(2026-10-05更新版、2026-10-15確認)に基づく。特定のブラウザやライブラリがこれらの値をどう見せるかは扱わない。
Closeフレームに入るもの
Closeはopcode 0x8 の制御フレームだ。本体(application data)は入れても入れなくてもよい(MAY)。本体がある場合、先頭の2バイトはネットワークバイトオーダーの符号なし16ビット整数、つまりステータスコードでなければならない(MUST)。その後ろにUTF-8の理由(reason)文字列を続けられる。
Closeフレームの本体
+----------------------+--------------------------------+
| ステータスコード 2バイト | 理由: UTF-8のバイト列(省略可) |
| 例: 0x03E8 = 1000 | |
+----------------------+--------------------------------+
制御フレームはすべてペイロードが125バイト以下でなければならず、分割(fragment)もできない(MUST)。コードで2バイト使うので、理由文字列に残るのは最大123バイトになる。UTF-8で1文字(コードポイント)が3バイトを占めるなら、41文字で123バイトを使い切る。制限は文字数ではなくエンコード後のバイト数にかかるので、実装では文字数ではなくUTF-8にエンコードしたあとのバイト長を確認する必要がある。
理由文字列の解釈はRFCでは定められていない。人が読める内容である保証もないため、クライアントはこれをエンドユーザーに表示してはならない(MUST NOT)。デバッグ用、あるいは接続を開いたスクリプトに情報を渡す用途と考えるとよい。
クロージングハンドシェイクの順序
RFCは、TCPのクローズ(FIN/ACK)とは別にWebSocketレベルのクローズ手順を置いている。TCPのクローズはエンドツーエンドで常に信頼できるわけではなく、通信を横取りするプロキシが間にいると特にそうだ、というのが理由である(1.4節、非規範)。順序は次のとおり。
A B
|--- Close(1000, "done") --------->| Aはこれ以降データフレームを送らない
| | Bはまだ Close を送っていないので応答が必要
|<-------------- Close(1000) ------| 受け取ったコードをそのまま返すことが多い
| |
双方が Close を送受信した → 下位のTCP接続を閉じる
(サーバーはすぐ閉じる。クライアントはサーバーが閉じるのを待つのが原則)
各段階の要求レベルは次のとおり(5.5.1節)。
- Closeを送った側は、その後データフレームを送ってはならない(MUST NOT)。
- Closeを受け取ったときにまだ送っていなければ、Closeで応答しなければならない(MUST)。できるだけ早く送るべきで(SHOULD)、送信中のメッセージがあればそれを送り終えてからにしてもよい(MAY)。応答では受け取ったコードをそのまま返すのが一般的だとRFCは書いているが、義務ではない。
- すでにCloseを送った側が、その後に届くデータを処理し続ける保証はない。
- Closeを送受信したあとは下位のTCP接続を閉じなければならない(MUST)。サーバーは直ちに閉じなければならず(MUST)、クライアントはサーバーが閉じるのを待つのが原則だが(SHOULD)、妥当な時間内にサーバーからTCPのクローズが来なければいつ閉じてもよい(MAY)。
- 双方が同時にCloseを送っても問題ない。どちらも送受信を済ませた状態になるので、接続を閉じればよい。
サーバーが先にTCPを閉じるべき理由は7.1.1節にある。先に閉じた側がTIME_WAIT状態を抱えることになり、それがクライアント側に残ると2MSLの間その接続を開き直せなくなるからだ。TIME_WAITそのものはTIME-WAITのソケットが溜まる理由で取り上げた。
Closeフレームをやり取りしたあとでTCP接続が閉じられた場合、RFCはその接続が「クリーンに(cleanly)」閉じられたと呼ぶ(7.1.4節)。接続の確立自体に失敗した場合も閉じられたものとして扱うが、クリーンな終了ではない。
ログに出るクローズコードはどう決まるか
ここで1005と1006の正体がわかる。7.1.5節は「WebSocket接続のクローズコード」を次のように定義している。
| 状況 | このエンドポイントのクローズコード |
|---|---|
| 最初に受け取ったCloseフレームにステータスコードがある | そのコード |
| 最初に受け取ったCloseフレームにステータスコードがない | 1005 |
| 接続が閉じたが、Closeフレームを一つも受け取っていない | 1006 |
基準は自分が送ったCloseではなく、自分が最初に受け取ったCloseだ。クローズ理由も同じ規則に従い、受け取ったフレームに文字列がなければ空文字列になる(7.1.6節)。
つまり1006が教えてくれるのは「Closeフレームを受け取る前に接続が終わった」という事実だけだ。RFCが挙げる例は下位のトランスポート接続が失われた場合である。相手のプロセスがCloseを送らずに終了したのか、途中のネットワークが接続を切ったのかといった原因は、この番号だけでは区別できない。原因を調べるには反対側のエンドポイントの記録も合わせて見る必要がある。
両側が同じコードを見る保証もない。RFC自身が例を挙げている。相手が送ったCloseがまだ自分の受信バッファにあり、アプリケーションがそれを読む前に独自に閉じることを決めてCloseを送る。すると両方のエンドポイントがCloseを送受信した状態になり、それぞれが相手の送ったコードをクローズコードとして記録する。ほぼ同時に双方がクローズを始めると、両側のログでコードが食い違うことがある。
RFC 6455が定義するコード
7.4.1節のコードは、Closeフレームを送るときに使ってよい(MAY)値だ。このうち3つはフレームに入れてはならない(MUST NOT)予約値である。
| コード | IANAの名前 | RFC 6455での意味 | フレームに入れてよいか |
|---|---|---|---|
| 1000 | Normal Closure | 接続を開いた目的を果たした正常終了 | はい |
| 1001 | Going Away | サーバーの停止やブラウザのページ遷移など、エンドポイントがいなくなる | はい |
| 1002 | Protocol error | プロトコルエラーによる切断 | はい |
| 1003 | Unsupported Data | 受け入れられない種類のデータを受信(例:テキストしか扱わないエンドポイントがバイナリを受信) | はい |
| 1004 | Reserved | 予約。意味は将来定められる可能性がある | 定義された意味なし |
| 1005 | No Status Rcvd | ステータスコードがなかったことを示す | いいえ |
| 1006 | Abnormal Closure | Closeフレームなしの異常終了を示す | いいえ |
| 1007 | Invalid frame payload data | メッセージの種類と合わないデータ(例:テキストメッセージ内のUTF-8でないバイト) | はい |
| 1008 | Policy Violation | ポリシー違反。より適切なコードがないときや、ポリシーの詳細を隠したいときの汎用コード | はい |
| 1009 | Message Too Big | 処理するには大きすぎるメッセージ | はい |
| 1010 | Mandatory Ext. | クライアントが期待した拡張をサーバーがネゴシエートしなかった。必要な拡張の一覧は理由に入れるのが原則(SHOULD) | はい(クライアントのみ) |
| 1011 | Internal Error | サーバーが予期しない状況のためにリクエストを処理できない | はい |
| 1015 | TLS handshake | TLSハンドシェイクの失敗(例:サーバー証明書を検証できない)を示す | いいえ |
いくつか補足しておく。
- サーバーが1010を使わない理由について、RFCはサーバーならWebSocketハンドシェイク自体を失敗させればよいからだと説明している。
- 1011の名前は、RFC 6455のIANA初期表(11.7節)では「Internal Server Error」、現在のIANAレジストリでは「Internal Error」となっている。レジストリのこの項目にはRFC 6455と並んで正誤表(errata)3227が参照として付いている。7.4.1節本文の説明は「サーバーが」で始まる。
- 1012(Service Restart)、1013(Try Again Later)、1014はRFC 6455の本文にはなく、IANAレジストリにだけある。参照先はIETF hybiメーリングリストの投稿だ。1014の説明は「サーバーがゲートウェイやプロキシとして動作していて、上流サーバーから不正な応答を受け取った。HTTPの502に近い」というもの。1012と1013はレジストリに名前しかないので、この記事では名前以上の意味を読み込まない。
さらに7.4節は、受け取った側がコードをどう解釈し、何をすべきかは定めないと明記している。1001を受け取ったら再接続する、1008ならログインし直させる、といった動作はアプリケーションが決めることだ。
コードの範囲:独自のコードはどこに置くか
7.4.2節と11.7節が範囲を分けている。
| 範囲 | 用途 | 登録 |
|---|---|---|
| 0–999 | 使わない | — |
| 1000–2999 | このプロトコル、将来の改訂、公開仕様のある拡張 | Standards Action、Specification Required、IESG Reviewのいずれか |
| 3000–3999 | ライブラリ、フレームワーク、アプリケーション | IANAに先着順(First Come First Served)で登録 |
| 4000–4999 | 私的利用(private use) | 登録できない。アプリケーション間の事前の取り決めで使う |
3000番台と4000番台のコードの解釈はRFCでは定められていない。2026-10-05に更新されたIANAレジストリで3000番台に登録されている値は3000(Unauthorized)、3003(Forbidden)、3008(Timeout)の3つで、残りは未割り当てだ。1016–2999も未割り当てである。
自分のサービスのクライアントとサーバーの間でだけ使うコードなら、4000–4999から選び、その意味を両側のコードで共有しておくのが範囲の趣旨に合う。多くのサービスが使うライブラリであれば、3000番台をIANAに登録するという方法がある。
1006のあとの再接続:バックオフが必要な理由
7.2.3節は、異常終了後の再接続についてかなり具体的に勧告している。障害が一時的なものでないのに、配布されたすべてのクライアントがすぐに、しつこく再接続を試みると、サーバーは事実上のサービス拒否攻撃を受けるのと同じ状況になり、回復が遅れたり難しくなったりしかねない、という理由だ。
- 再接続には何らかのバックオフを使うべきである(SHOULD)。
- 最初の再接続はランダムな時間だけ遅らせるべきである(SHOULD)。0〜5秒のランダムな値が妥当な初期遅延だとRFCは書いており、別の区間を選んでもよい(MAY)。
- 最初の試行が失敗したら、その後の試行はだんだん長く遅らせるべきである(SHOULD)。例として挙がっているのは上限付きの二進指数バックオフ(truncated binary exponential backoff)だ。
この勧告をコードにした例を示す。最初の0〜5秒はRFCが挙げている値で、その後の計算式と60秒の上限はRFCが定めたものではなく、この記事で選んだものだ。2回目以降の遅延は固定値で、5秒、10秒、20秒、40秒と倍々に伸び、その後は毎回60秒になる。
// attempt: 0 なら最初の再接続
function reconnectDelayMs(attempt: number): number {
if (attempt === 0) {
return Math.random() * 5_000; // 0〜5秒のランダム
}
// 5秒、10秒、20秒、40秒、以降は60秒で止める
return Math.min(60_000, 5_000 * 2 ** (attempt - 1));
}
ランダムに決める最初の遅延は、同じ瞬間に切断されたクライアントが同じ瞬間に戻ってこないよう、タイミングをばらけさせる役割を持つ。この例では、その後の遅延は固定値なので、ランダムさは加わらない。
まとめ:実装で確認すること
RFCの文言から直接出てくる規則と、それをもとにしたこの記事の提案を分けて書く。
RFC 6455の規則
- 1005、1006、1015はCloseフレームに入れない。
- 理由文字列は123バイト以内に収める(制御フレームの125バイト制限から計算)。
- クライアントは理由文字列をユーザーに表示しない。
- Closeを受け取ったらCloseで応答し、双方がやり取りしたあとはサーバーがすぐにTCPを閉じる。
- 異常終了後の再接続では、ランダムな初期遅延と、だんだん長くなる遅延を入れる。
この記事の提案
- 正常終了には1000を、サーバーの停止や再デプロイで接続を整理するときは1001を送る。
- サービス固有の終了理由は4000–4999で決め、クライアントとサーバーの両方に同じ表を持たせる。
- ログにはコードだけでなく、Closeフレームを実際に受け取ったかどうかも残す。1006を調べるときは反対側のエンドポイントのログと突き合わせる。