diff --git a/apps/desktop/src/lib/json-rpc-gateway-url-guard.test.ts b/apps/desktop/src/lib/json-rpc-gateway-url-guard.test.ts index 9989c669f5d4..7cedfe9c69c8 100644 --- a/apps/desktop/src/lib/json-rpc-gateway-url-guard.test.ts +++ b/apps/desktop/src/lib/json-rpc-gateway-url-guard.test.ts @@ -43,6 +43,12 @@ describe('JsonRpcGatewayClient connect() URL guard', () => { ) }) + it('rejects a malformed ws URL before opening a socket', async () => { + const client = new JsonRpcGatewayClient() + await expect(client.connect('ws://')).rejects.toThrow(/requires a ws:\/\/ or wss:\/\/ URL string/) + expect(client.connectionState).toBe('idle') + }) + it('keeps connection state idle on rejection', async () => { const client = new JsonRpcGatewayClient() await client.connect(undefined as unknown as string).catch(() => undefined) diff --git a/apps/shared/src/json-rpc-gateway.ts b/apps/shared/src/json-rpc-gateway.ts index de67806ddc4b..37714b8e2d4b 100644 --- a/apps/shared/src/json-rpc-gateway.ts +++ b/apps/shared/src/json-rpc-gateway.ts @@ -97,9 +97,25 @@ export class JsonRpcGatewayClient { async connect(wsUrl: string): Promise { // Refuse garbage; WebSocket coerces non-strings into // `ws:///[object%20Object]` (#68250 stale-emit boot loop). - if (typeof wsUrl !== 'string' || !/^wss?:\/\//i.test(wsUrl)) { + const invalidUrl = () => { const got = typeof wsUrl === 'string' ? JSON.stringify(wsUrl) : `type "${typeof wsUrl}"` - throw new Error(`gateway connect() requires a ws:// or wss:// URL string, got ${got}`) + + return new Error(`gateway connect() requires a ws:// or wss:// URL string, got ${got}`) + } + + if (typeof wsUrl !== 'string') { + throw invalidUrl() + } + + let url: URL + try { + url = new URL(wsUrl) + } catch { + throw invalidUrl() + } + + if (url.protocol !== 'ws:' && url.protocol !== 'wss:') { + throw invalidUrl() } if (this.socket?.readyState === WebSocket.OPEN || this.state === 'connecting') {