WebSocket Close コード早見表:正常終了・プロトコル違反・ポリシー違反の実務対応
WebSocket 通信を実装する際、接続の切断理由を適切に伝えることは、デバッグやエラーハンドリングにおいて非常に重要です。しかし、多数の Close コードが定義されており、どのコードをどの場面で使うべきか迷うことも多いでしょう。
本記事では、WebSocket Close コードの仕様を整理し、正常終了・プロトコル違反・ポリシー違反それぞれのコードについて、実務での使い分けと実装方法を解説します。エラーハンドリングの実装例も交えながら、現場で即活用できる知識をお届けしますね。
WebSocket Close コード早見表
| # | コード | 名称 | 分類 | 用途 | クライアント送信 | サーバー送信 |
|---|---|---|---|---|---|---|
| 1 | 1000 | Normal Closure | 正常終了 | 通常の切断処理 | ★★★ | ★★★ |
| 2 | 1001 | Going Away | 正常終了 | ページ遷移・サーバー停止 | ★★★ | ★★★ |
| 3 | 1002 | Protocol Error | プロトコル違反 | WebSocket 仕様違反 | ★☆☆ | ★★★ |
| 4 | 1003 | Unsupported Data | プロトコル違反 | 非対応データ型受信 | ★☆☆ | ★★★ |
| 5 | 1005 | No Status Rcvd | 予約済み | 送信禁止(内部用) | ☆☆☆ | ☆☆☆ |
| 6 | 1006 | Abnormal Closure | 予約済み | 送信禁止(内部用) | ☆☆☆ | ☆☆☆ |
| 7 | 1007 | Invalid Payload | ポリシー違反 | データ整合性エラー | ★☆☆ | ★★★ |
| 8 | 1008 | Policy Violation | ポリシー違反 | アプリケーションポリシー違反 | ★★☆ | ★★★ |
| 9 | 1009 | Message Too Big | ポリシー違反 | メッセージサイズ超過 | ★☆☆ | ★★★ |
| 10 | 1010 | Mandatory Extension | 拡張エラー | 必須拡張未対応 | ★★☆ | ☆☆☆ |
| 11 | 1011 | Internal Error | サーバーエラー | サーバー内部エラー | ☆☆☆ | ★★★ |
| 12 | 1012 | Service Restart | サーバー状態 | サービス再起動 | ☆☆☆ | ★★☆ |
| 13 | 1013 | Try Again Later | サーバー状態 | 一時的な過負荷 | ☆☆☆ | ★★☆ |
| 14 | 1014 | Bad Gateway | サーバーエラー | ゲートウェイエラー | ☆☆☆ | ★★☆ |
| 15 | 1015 | TLS Handshake | 予約済み | 送信禁止(内部用) | ☆☆☆ | ☆☆☆ |
| 16 | 3000-3999 | カスタム | アプリケーション定義 | ライブラリ・フレームワーク用 | ★★★ | ★★★ |
| 17 | 4000-4999 | カスタム | アプリケーション定義 | プライベート用途 | ★★★ | ★★★ |
重要度の見方
- ★★★:頻繁に使用する(実装必須)
- ★★☆:よく使用する(実装推奨)
- ★☆☆:稀に使用する(必要に応じて実装)
- ☆☆☆:使用禁止または自動設定
背景
WebSocket Close コードとは
WebSocket 通信では、接続を閉じる際に「なぜ切断したのか」を示す数値コード(Close Code)を送信できます。このコードは、RFC 6455 で標準化されており、クライアントとサーバー双方が切断理由を明示的に伝えるために使用されるものです。
mermaidsequenceDiagram
participant C as クライアント
participant S as サーバー
C->>S: WebSocket接続確立
Note over C,S: データ通信
alt 正常終了
C->>S: Close Frame (1000)
S->>C: Close Frame (1000)
else プロトコル違反
S->>C: Close Frame (1002)
else ポリシー違反
S->>C: Close Frame (1008)
end
Note over C,S: 接続終了
上記の図は、WebSocket 接続の確立から切断までの基本フローを示しています。切断時には、状況に応じた適切な Close コードを送信することで、相手側が切断理由を把握できます。
Close コードの仕様範囲
RFC 6455 では、Close コードを以下のように分類しています。
| # | コード範囲 | 用途 | 送信可否 |
|---|---|---|---|
| 1 | 0-999 | 未使用・予約済み | 送信禁止 |
| 2 | 1000-2999 | RFC 6455 標準コード | 仕様に従って送信可能 |
| 3 | 3000-3999 | ライブラリ・フレームワーク用 | 登録して使用可能 |
| 4 | 4000-4999 | プライベート用途 | 自由に使用可能 |
| 5 | 5000 以上 | 未定義 | 送信禁止 |
この分類により、標準化されたエラーコードとアプリケーション固有のコードを混同せずに管理できます。
Close コードの重要性
適切な Close コードを使用することで、以下のメリットが得られます。
デバッグの効率化 Close コードを見るだけで、切断が正常か異常か、どの種類のエラーかを即座に判断できます。ログに記録された Close コードから、問題の原因を素早く特定できるでしょう。
クライアント側の再接続制御 エラーの種類によって、再接続すべきか、ユーザーに通知すべきかを判断できます。例えば、1000(正常終了)なら再接続不要、1001(Going Away)ならページ再読み込み後に再接続といった制御が可能です。
運用監視の向上 Close コードを集計することで、どの種類のエラーが頻発しているかを把握できます。1009(Message Too Big)が多発していれば、メッセージサイズ制限の見直しが必要だとわかりますね。
課題
実務で直面する問題点
WebSocket Close コードを扱う際、開発者は以下のような課題に直面します。
コードの選択基準が不明確 RFC 仕様書には多数のコードが定義されていますが、実際にどのコードを使うべきか判断しづらい状況があります。例えば、認証エラーの場合、1008(Policy Violation)を使うべきか、カスタムコード(4000 番台)を使うべきか迷うことも多いでしょう。
mermaidflowchart TD
start["切断が必要"] --> question1{"正常な切断?"}
question1 -->|はい| code1000["1000 Normal<br/>Closure"]
question1 -->|いいえ| question2{"エラーの種類は?"}
question2 --> protocol["プロトコル<br/>違反"]
question2 --> policy["ポリシー<br/>違反"]
question2 --> server["サーバー<br/>エラー"]
protocol --> q_protocol{"具体的な原因は?"}
policy --> q_policy{"具体的な原因は?"}
server --> q_server{"具体的な原因は?"}
q_protocol --> code1002["1002 Protocol Error"]
q_protocol --> code1003["1003 Unsupported Data"]
q_policy --> code1007["1007 Invalid Payload"]
q_policy --> code1008["1008 Policy Violation"]
q_policy --> code1009["1009 Message Too Big"]
q_server --> code1011["1011 Internal Error"]
q_server --> code1013["1013 Try Again Later"]
この図は、Close コード選択時の判断フローを示しています。実務では、エラーの種類を正確に分類し、適切なコードを選択する必要があります。
送信禁止コードの誤使用 1005、1006、1015 といったコードは「予約済み」として定義されており、アプリケーションから直接送信してはいけません。しかし、これらのコードはブラウザやライブラリが内部的に使用するため、仕様を理解していないと混乱を招きます。
以下は、送信禁止コードを誤って使用した場合のエラー例です。
typescript// ❌ 誤った実装例:送信禁止コードを使用
const ws = new WebSocket('wss://example.com');
// 1006は送信禁止コードのため、エラーが発生する
ws.close(1006, 'Abnormal closure');
上記のコードを実行すると、以下のエラーが発生します。
yamlError: InvalidAccessError: Failed to execute 'close' on 'WebSocket':
The code must be either 1000, or between 3000 and 4999. 1006 is neither.
このエラーは、ブラウザが送信禁止コードの使用を検出し、処理を拒否したことを示しています。
エラーハンドリングの複雑化 Close コードに応じた適切な処理を実装するには、多数の条件分岐が必要になります。コードが増えるにつれて、保守性が低下する問題があります。
ドキュメント不足 RFC 仕様書は英語で書かれており、技術的な詳細が多く含まれています。日本語で実務に即した情報を得るのが難しく、初心者にとってハードルが高い状況です。
課題がもたらす影響
これらの課題を解決しないまま実装を進めると、以下のような問題が発生します。
| # | 問題 | 影響 |
|---|---|---|
| 1 | 不適切なコード使用 | デバッグ困難、誤った再接続制御 |
| 2 | エラーハンドリング漏れ | ユーザー体験の低下、障害の長期化 |
| 3 | ログの可読性低下 | 障害分析に時間がかかる |
| 4 | 仕様違反の実装 | ブラウザでエラー発生、動作不良 |
適切な Close コードの使用は、単なる「ベストプラクティス」ではなく、システムの信頼性とデバッグ効率に直結する重要な要素なのです。
解決策
Close コードの分類と選択基準
WebSocket Close コードを体系的に理解するため、用途別に分類し、それぞれの選択基準を整理します。
正常終了コード(1000-1001)
正常な切断時に使用するコードです。エラーではないため、クライアント側で再接続処理を行う必要はありません。
| # | コード | 名称 | 使用場面 |
|---|---|---|---|
| 1 | 1000 | Normal Closure | 意図的な切断、処理完了後の切断 |
| 2 | 1001 | Going Away | ページ遷移、タブクローズ、サーバーメンテナンス |
実装例:正常終了の処理
typescript// クライアント側:処理完了後の正常切断
const ws = new WebSocket('wss://example.com');
// データ送信完了後、正常に切断
function sendDataAndClose(data: string) {
ws.send(data);
// 1000: 正常終了を明示
ws.close(1000, 'Data sent successfully');
}
上記のコードでは、データ送信後に 1000 コードで正常終了を通知しています。reason には切断理由を記載することで、ログ分析時に有用な情報となります。
typescript// サーバー側:メンテナンス前の切断通知(Node.js + ws)
import WebSocket from 'ws';
const wss = new WebSocket.Server({ port: 8080 });
// メンテナンス開始時、全クライアントに通知
function startMaintenance() {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
// 1001: サーバー側の都合で切断
client.close(1001, 'Server maintenance started');
}
});
}
1001 コードは「サーバー側の都合で切断するが、異常ではない」ことを示します。クライアントは、メンテナンス終了後に再接続すればよいと判断できますね。
プロトコル違反コード(1002-1003)
WebSocket 仕様に違反した通信を検出した場合に使用します。主にサーバー側から送信されます。
| # | コード | 名称 | 使用場面 |
|---|---|---|---|
| 1 | 1002 | Protocol Error | フレーム形式違反、ハンドシェイク失敗 |
| 2 | 1003 | Unsupported Data | テキストフレームに非 UTF-8 データ |
実装例:プロトコル違反の検出
typescript// サーバー側:無効なメッセージ形式の検出
wss.on('connection', (ws) => {
ws.on('message', (data) => {
try {
// JSONフォーマットを期待
const parsed = JSON.parse(data.toString());
// 必須フィールドの検証
if (!parsed.type || !parsed.payload) {
// 1002: プロトコルレベルのエラー
ws.close(1002, 'Invalid message format');
return;
}
// 正常処理
handleMessage(parsed);
} catch (error) {
// JSONパースエラー
ws.close(1002, 'Message must be valid JSON');
}
});
});
上記の実装では、メッセージ形式の検証を行い、仕様に合わないデータを受信した場合に 1002 で切断しています。
typescript// サーバー側:非対応データ型の検出
ws.on('message', (data, isBinary) => {
// バイナリデータを受け付けないアプリの場合
if (isBinary) {
// 1003: 非対応データ型
ws.close(1003, 'Binary data not supported');
return;
}
// テキストデータのみ処理
processTextData(data.toString());
});
1003 は、アプリケーションが特定のデータ型に対応していないことを明示します。
ポリシー違反コード(1007-1009)
アプリケーション側で定義したポリシーに違反した場合に使用します。
| # | コード | 名称 | 使用場面 |
|---|---|---|---|
| 1 | 1007 | Invalid Payload | UTF-8 エラー、データ検証失敗 |
| 2 | 1008 | Policy Violation | 認証失敗、権限不足 |
| 3 | 1009 | Message Too Big | メッセージサイズ超過 |
実装例:ポリシー違反の処理
typescript// 認証トークンの検証
wss.on('connection', (ws, req) => {
const token = req.headers['authorization'];
if (!token) {
// 1008: 認証ポリシー違反
ws.close(1008, 'Authentication required');
return;
}
// トークン検証
if (!verifyToken(token)) {
ws.close(1008, 'Invalid authentication token');
return;
}
// 認証成功後の処理
setupAuthenticatedConnection(ws);
});
1008 は、アプリケーションレベルのポリシー違反全般に使用できる汎用的なコードです。認証・認可エラーでよく使われます。
typescript// メッセージサイズ制限の実装
const MAX_MESSAGE_SIZE = 1024 * 100; // 100KB
ws.on('message', (data) => {
// メッセージサイズチェック
if (data.length > MAX_MESSAGE_SIZE) {
// 1009: メッセージサイズ超過
ws.close(
1009,
`Message size limit exceeded: ${MAX_MESSAGE_SIZE} bytes`
);
return;
}
processMessage(data);
});
1009 を使用することで、クライアント側はメッセージサイズを調整して再送できます。
typescript// データ整合性チェック
ws.on('message', (data) => {
const message = data.toString();
// UTF-8エンコーディング検証
if (!isValidUTF8(message)) {
// 1007: データ整合性エラー
ws.close(1007, 'Invalid UTF-8 encoding');
return;
}
// ペイロードの署名検証
const payload = JSON.parse(message);
if (!verifySignature(payload)) {
ws.close(1007, 'Payload signature verification failed');
return;
}
processPayload(payload);
});
1007 は、データの整合性やエンコーディングに問題がある場合に使用します。
サーバーエラーコード(1011-1014)
サーバー側で発生したエラーを通知する際に使用します。
| # | コード | 名称 | 使用場面 |
|---|---|---|---|
| 1 | 1011 | Internal Error | 予期しないサーバーエラー |
| 2 | 1012 | Service Restart | サービス再起動 |
| 3 | 1013 | Try Again Later | 一時的な過負荷 |
| 4 | 1014 | Bad Gateway | プロキシ・ゲートウェイエラー |
実装例:サーバーエラーの通知
typescript// 内部エラーのハンドリング
ws.on('message', async (data) => {
try {
// データベース操作などの処理
await processData(data);
} catch (error) {
console.error('Internal error:', error);
// 1011: サーバー内部エラー
ws.close(1011, 'Internal server error occurred');
}
});
1011 は、予期しないエラーが発生した場合の汎用コードです。詳細なエラー情報はログに記録し、クライアントには簡潔な理由を通知します。
typescript// サーバー再起動時の通知
let isShuttingDown = false;
// グレースフルシャットダウン
process.on('SIGTERM', () => {
isShuttingDown = true;
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
// 1012: サービス再起動
client.close(1012, 'Service restarting');
}
});
// 全クライアント切断後にサーバー停止
setTimeout(() => process.exit(0), 5000);
});
// 新規接続の拒否
wss.on('connection', (ws) => {
if (isShuttingDown) {
ws.close(1012, 'Service restarting');
return;
}
// 通常処理
setupConnection(ws);
});
1012 を使用することで、クライアントは「サーバーが再起動中」と理解し、適切な待機時間後に再接続できます。
typescript// レートリミットの実装
const connectionCounts = new Map<string, number>();
const MAX_CONNECTIONS_PER_IP = 10;
wss.on('connection', (ws, req) => {
const clientIp = req.socket.remoteAddress || 'unknown';
const currentCount = connectionCounts.get(clientIp) || 0;
// 接続数制限チェック
if (currentCount >= MAX_CONNECTIONS_PER_IP) {
// 1013: 一時的な過負荷
ws.close(1013, 'Too many connections. Try again later');
return;
}
// 接続数をカウント
connectionCounts.set(clientIp, currentCount + 1);
ws.on('close', () => {
// 切断時にカウント減少
const count = connectionCounts.get(clientIp) || 0;
connectionCounts.set(clientIp, Math.max(0, count - 1));
});
});
1013 は、一時的な制限であることを示すため、クライアントは時間を置いて再試行できます。
カスタムコード(3000-4999)
アプリケーション固有のエラーコードとして使用できます。
| # | コード範囲 | 用途 | 登録要否 |
|---|---|---|---|
| 1 | 3000-3999 | ライブラリ・フレームワーク | IANA に登録推奨 |
| 2 | 4000-4999 | プライベート用途 | 登録不要 |
実装例:カスタムコードの使用
typescript// アプリケーション固有のエラーコード定義
enum CustomCloseCode {
// 4000番台:プライベート用途
SUBSCRIPTION_EXPIRED = 4000,
RATE_LIMIT_EXCEEDED = 4001,
INVALID_API_VERSION = 4002,
DUPLICATE_CONNECTION = 4003,
}
// サブスクリプション期限切れの検出
ws.on('message', async (data) => {
const userId = getUserIdFromToken(ws);
const subscription = await checkSubscription(userId);
if (!subscription.isActive) {
// カスタムコード:サブスクリプション期限切れ
ws.close(
CustomCloseCode.SUBSCRIPTION_EXPIRED,
'Subscription expired. Please renew.'
);
return;
}
processMessage(data);
});
カスタムコードを使用することで、アプリケーション固有のエラーを明確に区別できます。
typescript// レート制限の詳細な通知
const requestCounts = new Map<string, number[]>();
const RATE_LIMIT = 100; // 1分間に100リクエスト
const RATE_WINDOW = 60 * 1000; // 1分
ws.on('message', (data) => {
const userId = getUserIdFromToken(ws);
const now = Date.now();
// 過去1分間のリクエスト記録
const requests = requestCounts.get(userId) || [];
const recentRequests = requests.filter(
(time) => now - time < RATE_WINDOW
);
if (recentRequests.length >= RATE_LIMIT) {
// カスタムコード:レート制限超過
ws.close(
CustomCloseCode.RATE_LIMIT_EXCEEDED,
`Rate limit exceeded: ${RATE_LIMIT} requests per minute`
);
return;
}
// リクエスト記録を更新
recentRequests.push(now);
requestCounts.set(userId, recentRequests);
processMessage(data);
});
標準コード(1013)でも対応できますが、カスタムコードを使うことで、より詳細なエラー分類が可能になります。
Close コード使用時のベストプラクティス
mermaidflowchart TD
receive["Close Frame<br/>受信"] --> check_code{"コードの<br/>範囲は?"}
check_code -->|1000-1001| normal["正常終了<br/>処理"]
check_code -->|1002-1003| protocol_err["プロトコル<br/>違反処理"]
check_check_code -->|1007-1009| policy_err["ポリシー<br/>違反処理"]
check_code -->|1011-1014| server_err["サーバー<br/>エラー処理"]
check_code -->|4000-4999| custom["カスタム<br/>処理"]
normal --> log_info["INFO<br/>ログ記録"]
protocol_err --> log_warn["WARN<br/>ログ記録"]
policy_err --> log_warn
server_err --> log_error["ERROR<br/>ログ記録"]
custom --> log_custom["カスタム<br/>ログ記録"]
log_info --> decision1{"再接続<br/>必要?"}
log_warn --> decision2{"再試行<br/>可能?"}
log_error --> decision3{"復旧<br/>可能?"}
log_custom --> decision4{"カスタム<br/>判定"}
decision1 -->|不要| task_end["終了"]
decision1 -->|必要| reconnect["再接続"]
decision2 -->|はい| retry["再試行"]
decision2 -->|いいえ| notify_user["ユーザー<br/>通知"]
decision3 -->|はい| wait_retry["待機後<br/>再接続"]
decision3 -->|いいえ| notify_user
decision4 --> custom_action["カスタム<br/>アクション"]
この図は、Close コード受信時の処理フローを示しています。コードの範囲に応じて、適切なログレベルと再接続戦略を選択する必要があります。
ベストプラクティスまとめ
- 適切なコード選択:エラーの性質に応じて、最も具体的なコードを選択する
- reason 文字列の活用:デバッグに役立つ詳細情報を含める(ただし個人情報は除く)
- 送信禁止コードの回避:1005、1006、1015 は使用しない
- ログ記録の徹底:Close コードと理由を必ずログに記録する
- クライアント側の適切な処理:コードに応じた再接続戦略を実装する
具体例
実践的なエラーハンドリング実装
実務で使える、包括的なエラーハンドリングの実装例を示します。
クライアント側の実装
typescript// クライアント側:Closeコード別の処理クラス
class WebSocketClient {
private ws: WebSocket | null = null;
private url: string;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
constructor(url: string) {
this.url = url;
}
connect(): void {
this.ws = new WebSocket(this.url);
this.setupEventHandlers();
}
private setupEventHandlers(): void {
if (!this.ws) return;
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0; // リセット
};
this.ws.onclose = (event) => {
this.handleClose(event);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
上記のコードは、WebSocket 接続を管理する基本クラスです。close イベントを専用メソッドで処理することで、コード別の制御を実現します。
typescript // Closeコード別の処理メソッド
private handleClose(event: CloseEvent): void {
const { code, reason, wasClean } = event;
console.log(
`WebSocket closed: code=${code}, ` +
`reason="${reason}", clean=${wasClean}`
);
// コード別の処理
switch (code) {
case 1000: // Normal Closure
this.handleNormalClosure(reason);
break;
case 1001: // Going Away
this.handleGoingAway(reason);
break;
case 1002: // Protocol Error
case 1003: // Unsupported Data
this.handleProtocolError(code, reason);
break;
case 1007: // Invalid Payload
case 1008: // Policy Violation
case 1009: // Message Too Big
this.handlePolicyViolation(code, reason);
break;
case 1011: // Internal Error
this.handleServerError(reason);
break;
case 1012: // Service Restart
case 1013: // Try Again Later
this.handleTemporaryError(code, reason);
break;
case 1006: // Abnormal Closure
this.handleAbnormalClosure(reason);
break;
default:
if (code >= 4000 && code <= 4999) {
this.handleCustomCode(code, reason);
} else {
this.handleUnknownCode(code, reason);
}
}
}
switch 文で Close コードを分岐し、それぞれに適した処理を行います。カスタムコード(4000 番台)も適切に処理できます。
typescript // 正常終了の処理
private handleNormalClosure(reason: string): void {
console.info('Connection closed normally:', reason);
// 再接続不要
}
// Going Awayの処理
private handleGoingAway(reason: string): void {
console.info('Server going away:', reason);
// サーバーメンテナンス等の可能性
// 少し待ってから再接続
setTimeout(() => this.reconnect(), 5000);
}
// プロトコルエラーの処理
private handleProtocolError(code: number, reason: string): void {
console.error(
`Protocol error (${code}):`,
reason
);
// プロトコル違反は再接続しても解決しない
this.showUserError(
'アプリケーションエラーが発生しました。' +
'ページを再読み込みしてください。'
);
}
正常終了(1000)では再接続を行わず、Going Away(1001)では短時間後に再接続します。プロトコルエラー(1002-1003)はアプリケーション側の問題なので、ユーザーに通知します。
typescript // ポリシー違反の処理
private handlePolicyViolation(code: number, reason: string): void {
console.warn(
`Policy violation (${code}):`,
reason
);
if (code === 1008) {
// 認証エラーの可能性
this.showUserError(
'認証エラーが発生しました。' +
'ログインし直してください。'
);
// ログイン画面へリダイレクト
this.redirectToLogin();
} else if (code === 1009) {
// メッセージサイズ超過
this.showUserError(
'送信データが大きすぎます。' +
'データを分割して送信してください。'
);
}
}
// サーバーエラーの処理
private handleServerError(reason: string): void {
console.error('Server internal error:', reason);
// サーバー側の問題なので、少し待ってから再接続
setTimeout(() => this.reconnect(), 3000);
}
ポリシー違反(1007-1009)では、コードに応じてユーザーに適切なメッセージを表示します。サーバーエラー(1011)は一時的な可能性があるため、再接続を試みます。
typescript // 一時的エラーの処理
private handleTemporaryError(code: number, reason: string): void {
console.warn(
`Temporary error (${code}):`,
reason
);
// エクスポネンシャルバックオフで再接続
const delay = Math.min(
1000 * Math.pow(2, this.reconnectAttempts),
30000 // 最大30秒
);
console.info(`Reconnecting in ${delay}ms...`);
setTimeout(() => this.reconnect(), delay);
}
// 異常切断の処理
private handleAbnormalClosure(reason: string): void {
console.error('Abnormal closure:', reason);
// ネットワーク断等の可能性
this.reconnect();
}
// カスタムコードの処理
private handleCustomCode(code: number, reason: string): void {
console.warn(
`Custom close code (${code}):`,
reason
);
// アプリケーション固有の処理
switch (code) {
case 4000: // SUBSCRIPTION_EXPIRED
this.showUserError(
'サブスクリプションが期限切れです。'
);
this.redirectToSubscriptionPage();
break;
case 4001: // RATE_LIMIT_EXCEEDED
this.showUserError(
'リクエストが多すぎます。' +
'しばらく待ってから再試行してください。'
);
setTimeout(() => this.reconnect(), 60000);
break;
default:
console.warn('Unknown custom code:', code);
}
}
一時的エラー(1012-1013)ではエクスポネンシャルバックオフを使用し、過度な再接続を防ぎます。カスタムコードでは、アプリケーション固有の処理を実装できます。
typescript // 再接続処理
private reconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnect attempts reached');
this.showUserError(
'接続できませんでした。' +
'ページを再読み込みしてください。'
);
return;
}
this.reconnectAttempts++;
console.info(
`Reconnect attempt ${this.reconnectAttempts}/` +
`${this.maxReconnectAttempts}`
);
this.connect();
}
// ユーザーへのエラー表示
private showUserError(message: string): void {
// 実際の実装では、UIコンポーネントを使用
alert(message);
}
// ログインページへリダイレクト
private redirectToLogin(): void {
window.location.href = '/login';
}
// サブスクリプションページへリダイレクト
private redirectToSubscriptionPage(): void {
window.location.href = '/subscription';
}
}
再接続処理では、最大試行回数を設定し、無限ループを防ぎます。エラーメッセージはユーザーフレンドリーな日本語で表示します。
サーバー側の実装
typescript// サーバー側:包括的なエラーハンドリング
import WebSocket, { WebSocketServer } from 'ws';
interface ConnectionMetadata {
userId?: string;
authenticated: boolean;
messageCount: number;
connectedAt: number;
}
class WebSocketServerManager {
private wss: WebSocketServer;
private connections = new Map<WebSocket, ConnectionMetadata>();
constructor(port: number) {
this.wss = new WebSocketServer({ port });
this.setupServerHandlers();
}
private setupServerHandlers(): void {
this.wss.on('connection', (ws, req) => {
this.handleNewConnection(ws, req);
});
}
サーバー側では、各接続のメタデータを管理し、適切なエラーハンドリングを実現します。
typescript private handleNewConnection(
ws: WebSocket,
req: any
): void {
console.log('New connection from:', req.socket.remoteAddress);
// 接続メタデータの初期化
this.connections.set(ws, {
authenticated: false,
messageCount: 0,
connectedAt: Date.now(),
});
// 認証タイムアウト(30秒以内に認証が必要)
const authTimeout = setTimeout(() => {
if (!this.connections.get(ws)?.authenticated) {
ws.close(
1008,
'Authentication timeout'
);
}
}, 30000);
ws.on('message', (data) => {
this.handleMessage(ws, data, authTimeout);
});
ws.on('close', (code, reason) => {
this.handleConnectionClose(ws, code, reason);
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
ws.close(1011, 'Internal server error');
});
}
新規接続時に、認証タイムアウトを設定します。30 秒以内に認証されない場合、1008(Policy Violation)で切断します。
typescript private handleMessage(
ws: WebSocket,
data: any,
authTimeout: NodeJS.Timeout
): void {
const metadata = this.connections.get(ws);
if (!metadata) return;
// メッセージサイズチェック
const MAX_SIZE = 100 * 1024; // 100KB
if (data.length > MAX_SIZE) {
ws.close(
1009,
`Message too big: max ${MAX_SIZE} bytes`
);
return;
}
// メッセージ数カウント
metadata.messageCount++;
// レート制限チェック(1分間に100メッセージ)
const timeElapsed = Date.now() - metadata.connectedAt;
const messagesPerMinute =
(metadata.messageCount / timeElapsed) * 60000;
if (messagesPerMinute > 100) {
ws.close(
4001, // カスタムコード: RATE_LIMIT_EXCEEDED
'Rate limit exceeded: 100 messages per minute'
);
return;
}
// メッセージ処理
try {
const message = JSON.parse(data.toString());
this.processMessage(ws, message, authTimeout);
} catch (error) {
ws.close(
1002,
'Invalid JSON format'
);
}
}
メッセージ受信時に、サイズ制限とレート制限をチェックします。違反があれば、適切な Close コードで切断します。
typescript private processMessage(
ws: WebSocket,
message: any,
authTimeout: NodeJS.Timeout
): void {
const metadata = this.connections.get(ws);
if (!metadata) return;
// 認証メッセージの処理
if (message.type === 'auth') {
const isValid = this.validateToken(message.token);
if (!isValid) {
ws.close(
1008,
'Invalid authentication token'
);
return;
}
// 認証成功
clearTimeout(authTimeout);
metadata.authenticated = true;
metadata.userId = message.userId;
ws.send(JSON.stringify({
type: 'auth_success',
}));
return;
}
// 未認証の場合、他のメッセージを拒否
if (!metadata.authenticated) {
ws.close(
1008,
'Authentication required'
);
return;
}
// 認証済みメッセージの処理
this.handleAuthenticatedMessage(ws, message);
}
認証メッセージを処理し、トークンが無効な場合は 1008 で切断します。認証完了後、タイムアウトをクリアします。
typescript private handleAuthenticatedMessage(
ws: WebSocket,
message: any
): void {
try {
// メッセージタイプ別の処理
switch (message.type) {
case 'ping':
ws.send(JSON.stringify({ type: 'pong' }));
break;
case 'data':
this.processData(ws, message.payload);
break;
default:
ws.close(
1003,
`Unsupported message type: ${message.type}`
);
}
} catch (error) {
console.error('Message processing error:', error);
ws.close(1011, 'Internal server error');
}
}
private processData(ws: WebSocket, payload: any): void {
// データ処理ロジック
console.log('Processing data:', payload);
ws.send(JSON.stringify({
type: 'data_received',
status: 'success',
}));
}
private validateToken(token: string): boolean {
// 実際のトークン検証ロジック
return token && token.length > 0;
}
認証済みメッセージを処理し、未対応のメッセージタイプには 1003 で応答します。
typescript private handleConnectionClose(
ws: WebSocket,
code: number,
reason: Buffer
): void {
const metadata = this.connections.get(ws);
const reasonString = reason.toString();
console.log(
`Connection closed: code=${code}, ` +
`reason="${reasonString}", ` +
`userId=${metadata?.userId || 'unknown'}`
);
// メタデータをクリーンアップ
this.connections.delete(ws);
// ログレベルの判定
if (code === 1000 || code === 1001) {
console.info('Normal closure');
} else if (code >= 1002 && code <= 1003) {
console.warn('Protocol error');
} else if (code >= 1007 && code <= 1009) {
console.warn('Policy violation');
} else if (code >= 1011 && code <= 1014) {
console.error('Server error');
} else {
console.log('Custom or unknown code');
}
}
// グレースフルシャットダウン
shutdown(): void {
console.log('Shutting down WebSocket server...');
this.wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.close(1012, 'Server shutting down');
}
});
this.wss.close(() => {
console.log('WebSocket server closed');
});
}
}
接続終了時に、Close コードに応じた適切なログレベルで記録します。シャットダウン時には、全クライアントに 1012 で通知します。
ロギングとモニタリング
typescript// Closeコードの集計とモニタリング
class CloseCodeMonitor {
private codeStats = new Map<number, number>();
// Closeコードを記録
recordClose(code: number, reason: string): void {
const count = this.codeStats.get(code) || 0;
this.codeStats.set(code, count + 1);
// ログ出力
this.logCloseEvent(code, reason);
// アラート判定
this.checkAlerts(code);
}
private logCloseEvent(code: number, reason: string): void {
const timestamp = new Date().toISOString();
const codeName = this.getCodeName(code);
console.log(
`[${timestamp}] WebSocket Close: ` +
`${code} (${codeName}) - ${reason}`
);
}
Close コードを集計し、異常な傾向を検出できるようにします。
typescript private checkAlerts(code: number): void {
const count = this.codeStats.get(code) || 0;
const threshold = 10;
// 特定のコードが頻発している場合、アラート
if (count > threshold) {
if (code === 1002 || code === 1003) {
console.error(
`ALERT: Protocol errors (${code}) ` +
`exceeded threshold: ${count} occurrences`
);
} else if (code === 1011) {
console.error(
`ALERT: Internal server errors (1011) ` +
`exceeded threshold: ${count} occurrences`
);
}
}
}
private getCodeName(code: number): string {
const names: Record<number, string> = {
1000: 'Normal Closure',
1001: 'Going Away',
1002: 'Protocol Error',
1003: 'Unsupported Data',
1007: 'Invalid Payload',
1008: 'Policy Violation',
1009: 'Message Too Big',
1011: 'Internal Error',
1012: 'Service Restart',
1013: 'Try Again Later',
};
return names[code] || 'Unknown Code';
}
// 統計情報の取得
getStats(): Record<string, number> {
const stats: Record<string, number> = {};
this.codeStats.forEach((count, code) => {
const name = this.getCodeName(code);
stats[`${code}_${name}`] = count;
});
return stats;
}
}
Close コードの集計により、システムの健全性を監視できます。特定のコードが頻発している場合、アラートを発生させます。
エラーコード別の対応まとめ
以下の表は、各 Close コードに対するクライアント・サーバー双方の推奨対応をまとめたものです。
| # | コード | クライアント対応 | サーバー対応 |
|---|---|---|---|
| 1 | 1000 | 再接続不要 | ログ記録(INFO) |
| 2 | 1001 | 5 秒後に再接続 | ログ記録(INFO) |
| 3 | 1002 | ユーザー通知、ページ再読み込み推奨 | ログ記録(WARN)、バグ調査 |
| 4 | 1003 | ユーザー通知、サポート問い合わせ | ログ記録(WARN)、仕様確認 |
| 5 | 1007 | データ検証強化 | ログ記録(WARN)、検証ロジック改善 |
| 6 | 1008 | 認証し直し、ログイン画面へ | ログ記録(WARN)、認証ログ確認 |
| 7 | 1009 | データ分割送信 | ログ記録(WARN)、サイズ制限見直し |
| 8 | 1011 | 3 秒後に再接続 | ログ記録(ERROR)、エラー原因調査 |
| 9 | 1012 | 10 秒後に再接続 | ログ記録(INFO)、計画的再起動 |
| 10 | 1013 | エクスポネンシャルバックオフで再接続 | ログ記録(WARN)、負荷分散検討 |
この表を参考にすることで、Close コード受信時の対応を素早く判断できます。
まとめ
WebSocket Close コードは、単なる切断通知ではなく、システムの状態を正確に伝える重要な仕組みです。本記事では、RFC 6455 で定義された標準コードから、アプリケーション固有のカスタムコードまで、体系的に整理しました。
適切な Close コードを使用することで、以下のメリットが得られます。
デバッグ効率の向上 Close コードと reason 文字列を記録することで、障害発生時の原因特定が迅速化します。ログを見るだけで「認証エラー」「メッセージサイズ超過」「サーバー内部エラー」といった問題を即座に判別できるでしょう。
ユーザー体験の改善 エラーの種類に応じた適切なメッセージをユーザーに表示できます。「一時的なエラーです。しばらくお待ちください」「認証が必要です。ログインしてください」といった具体的な案内により、ユーザーの不安を軽減できます。
システムの信頼性向上 Close コードの集計・分析により、システムの健全性を監視できます。特定のエラーが頻発している場合、プロアクティブに対処することで、大規模障害を未然に防げるでしょう。
再接続制御の最適化 エラーの性質に応じた再接続戦略を実装することで、サーバー負荷を抑えつつ、必要な場合のみ再接続できます。エクスポネンシャルバックオフや最大試行回数の設定により、無限ループを防止できます。
WebSocket 通信を実装する際は、ぜひ本記事の早見表と実装例を参考にしてください。適切な Close コードの使用は、堅牢でメンテナンスしやすいリアルタイム通信システムの基盤となります。
エラーハンドリングを丁寧に実装することで、ユーザーにとって安心して使えるアプリケーションを提供できるでしょう。
関連リンク
- RFC 6455 - The WebSocket Protocol - WebSocket の公式仕様書
- MDN Web Docs - CloseEvent - CloseEvent のリファレンス
- WebSocket API - MDN - WebSocket API の詳細ドキュメント
- ws - npm - Node.js 用 WebSocket ライブラリ
- IANA WebSocket Close Code Registry - 公式 Close Code 登録リスト
articleWebSocket Close コード早見表:正常終了・プロトコル違反・ポリシー違反の実務対応
articleKubernetes で WebSocket:Ingress(NGINX/ALB) 設定とスティッキーセッションの実装手順
articleWebSocket のペイロード比較:JSON・MessagePack・Protobuf の速度とコスト検証
articleWebSocket RFC6455 を図解速習:OPCODE・マスキング・Close コードの要点 10 分まとめ
articleWebSocket SLO/SLI 設計:接続維持率・遅延・ドロップ率の目標値と計測方法
articleWebSocket が「200 OK で Upgrade されない」原因と対処:プロキシ・ヘッダー・TLS の落とし穴
articleWebSocket Close コード早見表:正常終了・プロトコル違反・ポリシー違反の実務対応
articleStorybook 品質ゲート運用:Lighthouse/A11y/ビジュアル差分を PR で自動承認
articleWebRTC で高精細 1080p/4K 画面共有:contentHint「detail」と DPI 最適化
articleSolidJS フォーム設計の最適解:コントロール vs アンコントロールドの棲み分け
articleWebLLM 使い方入門:チャット UI を 100 行で実装するハンズオン
articleShell Script と Ansible/Make/Taskfile の比較:小規模自動化の最適解を検証
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来