diff --git a/CHANGELOG.md b/CHANGELOG.md index ca39130d2..10a9ceb8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to - 🐛(backend) skip session creation for the liveness probe - 🐛(frontend) preserve page titles when adding an emoji #2586 - 🐛(frontend) hide the selection highlight on presenter images #2665 +- 🐛(y-provider) prevent process crash on malformed websocket frames #2673 - ♿️(frontend) restore presenter focus trapping after share links #2533 ## [v5.6.1] - 2026-09-04 diff --git a/src/frontend/servers/y-provider/__tests__/collaborationWSHandler.test.ts b/src/frontend/servers/y-provider/__tests__/collaborationWSHandler.test.ts new file mode 100644 index 000000000..f046c4aed --- /dev/null +++ b/src/frontend/servers/y-provider/__tests__/collaborationWSHandler.test.ts @@ -0,0 +1,84 @@ +import { EventEmitter } from 'node:events'; + +import { Request } from 'express'; +import { describe, expect, test, vi } from 'vitest'; +import { WebSocket } from 'ws'; + +vi.mock('@/servers/hocuspocusServer', () => ({ + hocuspocusServer: { + hocuspocus: { + handleConnection: vi.fn(), + }, + }, +})); + +import { collaborationWSHandler } from '@/handlers/collaborationWSHandler'; +import { hocuspocusServer } from '@/servers/hocuspocusServer'; + +const handleConnectionMock = vi.spyOn( + hocuspocusServer.hocuspocus, + 'handleConnection', +); + +const createFakeWs = () => { + const ws = new EventEmitter() as unknown as WebSocket; + const closeMock = vi.fn(); + ws.close = closeMock; + return { ws, closeMock }; +}; + +describe('collaborationWSHandler', () => { + test('forwards the connection to hocuspocus', () => { + const { ws } = createFakeWs(); + const req = {} as Request; + + collaborationWSHandler(ws, req); + + expect(handleConnectionMock).toHaveBeenCalledWith(ws, req); + }); + + test('does not crash the process when the socket emits an unexpected "error" event', () => { + const consoleErrorMock = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined); + const { ws } = createFakeWs(); + + collaborationWSHandler(ws, {} as Request); + + const wsError = Object.assign(new Error('Invalid WebSocket frame'), { + code: 'WS_ERR_UNEXPECTED_RSV_2_3', + }); + + // Without an 'error' listener, EventEmitter would throw here and crash + // the process - this call must not throw. + expect(() => ws.emit('error', wsError)).not.toThrow(); + + expect(consoleErrorMock).toHaveBeenCalledWith( + 'WebSocket connection error:', + wsError, + ); + + consoleErrorMock.mockRestore(); + }); + + test('closes the socket and logs if handleConnection throws synchronously', () => { + const consoleErrorMock = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined); + const error = new Error('boom'); + handleConnectionMock.mockImplementationOnce(() => { + throw error; + }); + + const { ws, closeMock } = createFakeWs(); + collaborationWSHandler(ws, {} as Request); + + expect(closeMock).toHaveBeenCalled(); + expect(consoleErrorMock).toHaveBeenCalledWith( + 'Failed to handle WebSocket connection:', + error, + ); + + consoleErrorMock.mockRestore(); + }); +}); diff --git a/src/frontend/servers/y-provider/src/handlers/collaborationWSHandler.ts b/src/frontend/servers/y-provider/src/handlers/collaborationWSHandler.ts index 8890ad0b4..633e6be25 100644 --- a/src/frontend/servers/y-provider/src/handlers/collaborationWSHandler.ts +++ b/src/frontend/servers/y-provider/src/handlers/collaborationWSHandler.ts @@ -4,6 +4,10 @@ import * as ws from 'ws'; import { hocuspocusServer } from '@/servers/hocuspocusServer'; export const collaborationWSHandler = (ws: ws.WebSocket, req: Request) => { + ws.on('error', (error) => { + console.error('WebSocket connection error:', error); + }); + try { hocuspocusServer.hocuspocus.handleConnection(ws, req); } catch (error) {