http.Server#closeAllConnections()はWebSocket upgrade後のソケットを対象にせず、放置するとserver.close()が無期限ハングする
Node.jsのhttp.ServerでWebSocket(wsライブラリ等)をnoServer構成で扱う場合、'upgrade'イベントでソケットをwsサーバーへ引き渡した後は、そのソケットはhttp.Serverの通常のHTTP接続追跡から実質的に外れる。Node 18.2+で導入されたserver.closeAllConnections()は「確立済みの全接続を強制close」する機能だが、upgrade済み(ハイジャック済み)のソケットには効かない。この状態でws接続を明示的にcloseせずにserver.close(callback)を呼ぶと、callbackは永久に呼ばれずプロセスがハングする。
再現条件: WebSocketServerを{noServer: true}で構成しhttp.Serverの'upgrade'イベントからhandleUpgradeする実装で、テストなどが何らかの理由(アサーション失敗による早期return、例外送出など)でws接続をcloseし忘れたまま、finally節でserver.close()を呼ぶケース。特にテストコードでは「正常系だけを想定してws.close()を呼ぶ分岐」と「異常系(接続に失敗するはずが実装バグで意図せず成功してしまう分岐)でws.close()を呼ばない」という非対称な後始末になりやすく、対照実装(バグを意図的に注入した検証用コード)を流したときに初めて顕在化しやすい。
対処: server.close()を呼ぶ前に、生成した全wsクライアントに対して明示的にws.terminate()(close handshakeを待たない強制切断)を呼ぶ。正常系のためのws.close()+closeイベント待ちに加えて、finally節では「readyStateがCLOSEDでなければterminateする」という無条件の後始末を必ず入れる。テストヘルパーでws接続を張るユーティリティを作る場合は、返り値に生成済みwsを含め、呼び出し側がfinallyで一括terminateできるようにしておくと、アサーション失敗時にも後続のserver.close()がハングしない。
合わせて、接続確立を待つPromiseベースのヘルパー('open'/'unexpected-response'/'error'イベント待ち)には必ずタイムアウトの安全弁を入れる。サーバー側の実装ミスでハンドシェイクがopen/unexpected-response/errorのいずれも発火させない壊れ方をした場合(例: ws側のhandleProtocolsがfalseを返したときの挙動が想定と異なる等)、タイムアウトなしのPromiseはテスト全体を無期限にハングさせる。識別反証(意図的にバグを注入した対照実装を実際に実行して失敗を確認する検証手法)を境界のある実行時間で完走させるためにも、接続待ち・close待ちの両方にタイムアウトを持たせておくことが実務上重要。
確認方法: 素朴なhttp.Server+wsのnoServer構成で、クライアント接続をopenさせたままserver.closeAllConnections(); server.close(cb)を呼び、cbが一定時間内に呼ばれるかを検証すると再現できる。