From 6672f8a6d6241c4b643990d6f48315e9d37c08a2 Mon Sep 17 00:00:00 2001 From: sean0407 Date: Wed, 21 Oct 2026 15:24:34 +0800 Subject: [PATCH] fix(dev-server-core): reject websocket failures before open --- .../src/web-sockets/webSocketsPlugin.ts | 20 +++++- .../test/web-sockets/webSocketsPlugin.test.ts | 71 +++++++++++++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/packages/dev-server-core/src/web-sockets/webSocketsPlugin.ts b/packages/dev-server-core/src/web-sockets/webSocketsPlugin.ts index d3f702be6..a815bc782 100644 --- a/packages/dev-server-core/src/web-sockets/webSocketsPlugin.ts +++ b/packages/dev-server-core/src/web-sockets/webSocketsPlugin.ts @@ -164,13 +164,27 @@ function setupWebSocket() { 'WebSocket' in window ? new WebSocket(webSocketUrl) : null; - webSocketOpened = new Promise(resolve => { + webSocketOpened = new Promise((resolve, reject) => { if (!webSocket) { resolve(); } else { - webSocket.addEventListener('open', () => { + const onOpen = () => { + cleanup(); resolve(); - }); + }; + const onFailure = () => { + cleanup(); + reject(new Error('WebSocket connection failed before opening')); + }; + const cleanup = () => { + webSocket.removeEventListener('open', onOpen); + webSocket.removeEventListener('error', onFailure); + webSocket.removeEventListener('close', onFailure); + }; + + webSocket.addEventListener('open', onOpen); + webSocket.addEventListener('error', onFailure); + webSocket.addEventListener('close', onFailure); } }); let messageId = 0; diff --git a/packages/dev-server-core/test/web-sockets/webSocketsPlugin.test.ts b/packages/dev-server-core/test/web-sockets/webSocketsPlugin.test.ts index 039f9b5e8..cf62fca5c 100644 --- a/packages/dev-server-core/test/web-sockets/webSocketsPlugin.test.ts +++ b/packages/dev-server-core/test/web-sockets/webSocketsPlugin.test.ts @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { assertIncludes, assertNotIncludes } from '../../../../test-helpers/node.js'; +import { NAME_WEB_SOCKET_IMPORT } from '../../dist/web-sockets/WebSocketsManager.js'; import { webSocketScript } from '../../dist/web-sockets/webSocketsPlugin.js'; import { createTestServer } from '../helpers.ts'; @@ -87,4 +88,74 @@ describe('webSocketsPlugin', () => { server.stop(); } }); + + it('rejects a message when the websocket closes before opening', async () => { + const { server, host } = await createTestServer({ + injectWebSocket: true, + plugins: [{ name: 'test', injectWebSocket: true }], + }); + const originalGlobals = new Map( + ['window', 'navigator', 'WebSocket'].map(name => [ + name, + Object.getOwnPropertyDescriptor(globalThis, name), + ]), + ); + + try { + const response = await fetch(`${host}${NAME_WEB_SOCKET_IMPORT}`); + assert.equal(response.status, 200); + const source = await response.text(); + const fakeWindow = new EventTarget(); + Reflect.set(fakeWindow, 'parent', fakeWindow); + + class FakeWebSocket extends EventTarget { + send() {} + } + + Reflect.set(fakeWindow, 'WebSocket', FakeWebSocket); + Object.defineProperty(globalThis, 'window', { + configurable: true, + value: fakeWindow, + }); + Object.defineProperty(globalThis, 'navigator', { + configurable: true, + value: { userAgent: '' }, + }); + Object.defineProperty(globalThis, 'WebSocket', { + configurable: true, + value: FakeWebSocket, + }); + + const client = await import( + `data:text/javascript;base64,${Buffer.from(source).toString('base64')}` + ); + const pendingMessage = client.sendMessage({ type: 'wtr-session-started' }); + const messageOutcome = pendingMessage.then( + () => ({ type: 'resolved' as const }), + error => ({ type: 'rejected' as const, error }), + ); + + client.webSocket.dispatchEvent(new Event('close')); + const outcome = await Promise.race([ + messageOutcome, + new Promise<{ type: 'timeout' }>(resolve => { + setTimeout(() => resolve({ type: 'timeout' }), 100); + }), + ]); + + assert.equal(outcome.type, 'rejected'); + if (outcome.type === 'rejected') { + assert.match(outcome.error.message, /WebSocket connection failed before opening/); + } + } finally { + originalGlobals.forEach((descriptor, name) => { + if (descriptor) { + Object.defineProperty(globalThis, name, descriptor); + } else { + Reflect.deleteProperty(globalThis, name); + } + }); + server.stop(); + } + }); });