Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/true-lions-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/service-rsocket-router': patch
---

Fix uncaught error on invalid websocket frame
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@ export class WebsocketDuplexConnection extends Deferred implements DuplexConnect
) => Multiplexer & Demultiplexer & FrameHandler,
rawSocket: WebSocket.WebSocket
): void {
const handleInitialError = (error: Error) => {
// Protocol errors can occur before ws emits the first data event and a
// WebsocketDuplexConnection is constructed. The duplex still needs an
// error listener during that window, otherwise Node treats the error as
// unhandled and terminates the process.
logger.warn(`Error in WebSocket duplex connection: ${error}`);
socket.end();
};
socket.on('error', handleInitialError);

socket.once('data', async (buffer) => {
let frame: Frame | undefined = undefined;
try {
Expand All @@ -140,6 +150,7 @@ export class WebsocketDuplexConnection extends Deferred implements DuplexConnect
}

const connection = new WebsocketDuplexConnection(socket, frame, multiplexerDemultiplexerFactory, rawSocket);
socket.removeListener('error', handleInitialError);
if (connection.done) {
return;
}
Expand Down
33 changes: 33 additions & 0 deletions packages/rsocket-router/test/src/socket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,39 @@ describe('Sockets', () => {
await vi.waitFor(() => expect(rawSocket.readyState).equals(rawSocket.CLOSED), { timeout: 3000 });
});

it('should handle WebSocket protocol errors before the initial frame', async () => {
const transport = new WebsocketServerTransport({
wsCreator: () => server
});

const rSocketServer = new RSocketServer({
transport,
acceptor: {
accept: async () => {
return {};
}
}
});

await rSocketServer.bind();

const client = new WebSocket.WebSocket(WS_ADDRESS);
client.on('error', () => {});
await new Promise<void>((resolve) => {
client.once('open', () => resolve());
});

const closed = new Promise<number>((resolve) => {
client.once('close', (code) => resolve(code));
});

// FIN + RSV2 + RSV3 + text opcode, followed by an empty masked payload.
const invalidFrame = Buffer.from([0b1011_0001, 0b1000_0000, 0x00, 0x00, 0x00, 0x00]);
(client as any)._socket.write(invalidFrame);

expect(await closed).toEqual(1002);
});

/**
* The server should handle cases where the client closes the WebSocket connection
* at any point in the handshaking process. This test will create 100 connections which
Expand Down
Loading