From dd2483aaa9d751925de8762416b63cd58a936053 Mon Sep 17 00:00:00 2001 From: Leo6Leo <36619969+Leo6Leo@users.noreply.github.com> Date: Wed, 29 Apr 2026 23:19:15 -0400 Subject: [PATCH] OCPBUGS-84721: Fix WebSocket InvalidStateError in pod terminal WSFactory.send() only checked if the WebSocket instance existed before calling send(), which throws InvalidStateError when the connection is still in CONNECTING state. This race condition occurs during terminal reconnects or container switches when resize/input events fire before the handshake completes. Guard send() with a readyState === OPEN check to silently drop messages on connections that are not yet ready. --- .../utils/k8s/__tests__/ws-factory.spec.ts | 86 +++++++++++++++++++ .../src/utils/k8s/ws-factory.ts | 4 +- 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/__tests__/ws-factory.spec.ts diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/__tests__/ws-factory.spec.ts b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/__tests__/ws-factory.spec.ts new file mode 100644 index 00000000000..ced56c74b91 --- /dev/null +++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/__tests__/ws-factory.spec.ts @@ -0,0 +1,86 @@ +import { WSFactory } from '../ws-factory'; + +const CONNECTING = 0; +const OPEN = 1; +const CLOSING = 2; +const CLOSED = 3; + +const createMockWS = () => ({ + readyState: CONNECTING, + close: jest.fn(), + send: jest.fn(), + onopen: null, + onclose: null, + onerror: null, + onmessage: null, +}); + +let lastMockWS: ReturnType; + +const MockWebSocket = Object.assign( + jest.fn().mockImplementation(() => { + lastMockWS = createMockWS(); + return lastMockWS; + }), + { CONNECTING, OPEN, CLOSING, CLOSED }, +); + +(global as any).WebSocket = MockWebSocket; + +const createWSFactory = () => + new WSFactory('test', { + host: 'wss://localhost', + path: '/test', + subprotocols: [], + }); + +describe('WSFactory', () => { + beforeEach(() => { + MockWebSocket.mockClear(); + }); + + describe('send', () => { + it('should send data when readyState is OPEN', () => { + const ws = createWSFactory(); + lastMockWS.readyState = OPEN; + + ws.send('test-data'); + + expect(lastMockWS.send).toHaveBeenCalledWith('test-data'); + }); + + it('should not send data when readyState is CONNECTING', () => { + const ws = createWSFactory(); + lastMockWS.readyState = CONNECTING; + + ws.send('test-data'); + + expect(lastMockWS.send).not.toHaveBeenCalled(); + }); + + it('should not send data when readyState is CLOSING', () => { + const ws = createWSFactory(); + lastMockWS.readyState = CLOSING; + + ws.send('test-data'); + + expect(lastMockWS.send).not.toHaveBeenCalled(); + }); + + it('should not send data when readyState is CLOSED', () => { + const ws = createWSFactory(); + lastMockWS.readyState = CLOSED; + + ws.send('test-data'); + + expect(lastMockWS.send).not.toHaveBeenCalled(); + }); + + it('should not throw when ws is destroyed', () => { + const ws = createWSFactory(); + ws.destroy(); + + expect(() => ws.send('test-data')).not.toThrow(); + }); + }); +}); diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/ws-factory.ts b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/ws-factory.ts index 95b04a6da37..44f07a57103 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/ws-factory.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/ws-factory.ts @@ -344,6 +344,8 @@ export class WSFactory { } send(data: Parameters[0]) { - this.ws && this.ws.send(data); + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(data); + } } }