🐛(y-provider) prevent process crash on malformed websocket frames

The ws library emits an unhandled 'error' event when a client sends a
frame with unexpected RSV bits (e.g. permessage-deflate mismatch),
which was crashing the whole y-provider process since no listener was
attached to the socket. Add an error listener to log and drop the
offending connection instead.
This commit is contained in:
Anthony LC
2026-09-11 10:57:25 +02:00
parent e68c604ead
commit afc11adc52
3 changed files with 89 additions and 0 deletions
+1
View File
@@ -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
@@ -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();
});
});
@@ -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) {