/** * @file websocket-shutdown.test.js * @description Regression test for the shutdown race in server/websocket.js: * `closeWebSocket()` nulls both WebSocketServer references but cannot * unregister the `server.on("upgrade")` listeners it installed, so an upgrade * arriving mid-shutdown used to throw an unhandled TypeError * ("Cannot read properties of null (reading 'handleUpgrade')") and kill the * process. Uses a real http.Server and raw upgrade requests — no ws client. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it } = require("node:test"); const assert = require("node:assert/strict"); const http = require("node:http"); const net = require("node:net"); const ws = require("../websocket"); /** Send a raw WS upgrade for `path` and resolve once the socket settles. */ function rawUpgrade(port, path) { return new Promise((resolve) => { const socket = net.connect(port, "127.0.0.1", () => { socket.write( `GET ${path} HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\n` + `Upgrade: websocket\r\nConnection: Upgrade\r\n` + `Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n` ); }); let body = ""; socket.on("data", (d) => { body += d.toString("utf8"); }); socket.on("close", () => resolve(body)); socket.on("error", () => resolve(body)); }); } describe("websocket shutdown race", () => { it("drops upgrades that land after closeWebSocket instead of crashing", async () => { const server = http.createServer((req, res) => res.end("ok")); ws.initWebSocket(server); ws.initPtyWebSocket(server); await new Promise((r) => server.listen(0, "127.0.0.1", r)); const { port } = server.address(); // The shutdown the real server performs on SIGTERM. ws.closeWebSocket(); // Both paths, because each has its own upgrade listener and its own null // reference. An unguarded handleUpgrade here takes the process down, so // simply reaching the assertions below is the test passing. await rawUpgrade(port, "/ws"); await rawUpgrade(port, "/ws-pty/ccam-lane-1"); assert.equal(ws.getConnectionCount(), 0); await new Promise((r) => server.close(r)); }); });