T-CREATOR

WebSocket Close コード早見表:正常終了・プロトコル違反・ポリシー違反の実務対応

WebSocket Close コード早見表:正常終了・プロトコル違反・ポリシー違反の実務対応

WebSocket 通信を実装する際、接続の切断理由を適切に伝えることは、デバッグやエラーハンドリングにおいて非常に重要です。しかし、多数の Close コードが定義されており、どのコードをどの場面で使うべきか迷うことも多いでしょう。

本記事では、WebSocket Close コードの仕様を整理し、正常終了・プロトコル違反・ポリシー違反それぞれのコードについて、実務での使い分けと実装方法を解説します。エラーハンドリングの実装例も交えながら、現場で即活用できる知識をお届けしますね。

WebSocket Close コード早見表

#コード名称分類用途クライアント送信サーバー送信
11000Normal Closure正常終了通常の切断処理★★★★★★
21001Going Away正常終了ページ遷移・サーバー停止★★★★★★
31002Protocol Errorプロトコル違反WebSocket 仕様違反★☆☆★★★
41003Unsupported Dataプロトコル違反非対応データ型受信★☆☆★★★
51005No Status Rcvd予約済み送信禁止(内部用)☆☆☆☆☆☆
61006Abnormal Closure予約済み送信禁止(内部用)☆☆☆☆☆☆
71007Invalid Payloadポリシー違反データ整合性エラー★☆☆★★★
81008Policy Violationポリシー違反アプリケーションポリシー違反★★☆★★★
91009Message Too Bigポリシー違反メッセージサイズ超過★☆☆★★★
101010Mandatory Extension拡張エラー必須拡張未対応★★☆☆☆☆
111011Internal Errorサーバーエラーサーバー内部エラー☆☆☆★★★
121012Service Restartサーバー状態サービス再起動☆☆☆★★☆
131013Try Again Laterサーバー状態一時的な過負荷☆☆☆★★☆
141014Bad Gatewayサーバーエラーゲートウェイエラー☆☆☆★★☆
151015TLS Handshake予約済み送信禁止(内部用)☆☆☆☆☆☆
163000-3999カスタムアプリケーション定義ライブラリ・フレームワーク用★★★★★★
174000-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 コードを以下のように分類しています。

#コード範囲用途送信可否
10-999未使用・予約済み送信禁止
21000-2999RFC 6455 標準コード仕様に従って送信可能
33000-3999ライブラリ・フレームワーク用登録して使用可能
44000-4999プライベート用途自由に使用可能
55000 以上未定義送信禁止

この分類により、標準化されたエラーコードとアプリケーション固有のコードを混同せずに管理できます。

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)

正常な切断時に使用するコードです。エラーではないため、クライアント側で再接続処理を行う必要はありません。

#コード名称使用場面
11000Normal Closure意図的な切断、処理完了後の切断
21001Going 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 仕様に違反した通信を検出した場合に使用します。主にサーバー側から送信されます。

#コード名称使用場面
11002Protocol Errorフレーム形式違反、ハンドシェイク失敗
21003Unsupported 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)

アプリケーション側で定義したポリシーに違反した場合に使用します。

#コード名称使用場面
11007Invalid PayloadUTF-8 エラー、データ検証失敗
21008Policy Violation認証失敗、権限不足
31009Message 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)

サーバー側で発生したエラーを通知する際に使用します。

#コード名称使用場面
11011Internal Error予期しないサーバーエラー
21012Service Restartサービス再起動
31013Try Again Later一時的な過負荷
41014Bad 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)

アプリケーション固有のエラーコードとして使用できます。

#コード範囲用途登録要否
13000-3999ライブラリ・フレームワークIANA に登録推奨
24000-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 コード受信時の処理フローを示しています。コードの範囲に応じて、適切なログレベルと再接続戦略を選択する必要があります。

ベストプラクティスまとめ

  1. 適切なコード選択:エラーの性質に応じて、最も具体的なコードを選択する
  2. reason 文字列の活用:デバッグに役立つ詳細情報を含める(ただし個人情報は除く)
  3. 送信禁止コードの回避:1005、1006、1015 は使用しない
  4. ログ記録の徹底:Close コードと理由を必ずログに記録する
  5. クライアント側の適切な処理:コードに応じた再接続戦略を実装する

具体例

実践的なエラーハンドリング実装

実務で使える、包括的なエラーハンドリングの実装例を示します。

クライアント側の実装

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 コードに対するクライアント・サーバー双方の推奨対応をまとめたものです。

#コードクライアント対応サーバー対応
11000再接続不要ログ記録(INFO)
210015 秒後に再接続ログ記録(INFO)
31002ユーザー通知、ページ再読み込み推奨ログ記録(WARN)、バグ調査
41003ユーザー通知、サポート問い合わせログ記録(WARN)、仕様確認
51007データ検証強化ログ記録(WARN)、検証ロジック改善
61008認証し直し、ログイン画面へログ記録(WARN)、認証ログ確認
71009データ分割送信ログ記録(WARN)、サイズ制限見直し
810113 秒後に再接続ログ記録(ERROR)、エラー原因調査
9101210 秒後に再接続ログ記録(INFO)、計画的再起動
101013エクスポネンシャルバックオフで再接続ログ記録(WARN)、負荷分散検討

この表を参考にすることで、Close コード受信時の対応を素早く判断できます。

まとめ

WebSocket Close コードは、単なる切断通知ではなく、システムの状態を正確に伝える重要な仕組みです。本記事では、RFC 6455 で定義された標準コードから、アプリケーション固有のカスタムコードまで、体系的に整理しました。

適切な Close コードを使用することで、以下のメリットが得られます。

デバッグ効率の向上 Close コードと reason 文字列を記録することで、障害発生時の原因特定が迅速化します。ログを見るだけで「認証エラー」「メッセージサイズ超過」「サーバー内部エラー」といった問題を即座に判別できるでしょう。

ユーザー体験の改善 エラーの種類に応じた適切なメッセージをユーザーに表示できます。「一時的なエラーです。しばらくお待ちください」「認証が必要です。ログインしてください」といった具体的な案内により、ユーザーの不安を軽減できます。

システムの信頼性向上 Close コードの集計・分析により、システムの健全性を監視できます。特定のエラーが頻発している場合、プロアクティブに対処することで、大規模障害を未然に防げるでしょう。

再接続制御の最適化 エラーの性質に応じた再接続戦略を実装することで、サーバー負荷を抑えつつ、必要な場合のみ再接続できます。エクスポネンシャルバックオフや最大試行回数の設定により、無限ループを防止できます。

WebSocket 通信を実装する際は、ぜひ本記事の早見表と実装例を参考にしてください。適切な Close コードの使用は、堅牢でメンテナンスしやすいリアルタイム通信システムの基盤となります。

エラーハンドリングを丁寧に実装することで、ユーザーにとって安心して使えるアプリケーションを提供できるでしょう。

関連リンク