Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions packages/dev-server-core/src/web-sockets/webSocketsPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
71 changes: 71 additions & 0 deletions packages/dev-server-core/test/web-sockets/webSocketsPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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();
}
});
});