From fce3b78706a3fab5f085f7d7edb0cbed4817d28a Mon Sep 17 00:00:00 2001 From: Nyk <0xnykcd@googlemail.com> Date: Thu, 5 Mar 2026 12:55:46 +0700 Subject: [PATCH] fix(gateway): improve remote websocket URL handling and client-id errors --- src/components/panels/multi-gateway-panel.tsx | 10 +++- src/lib/__tests__/gateway-url.test.ts | 36 ++++++++++++ src/lib/gateway-url.ts | 56 +++++++++++++++++++ src/lib/websocket.ts | 5 ++ 4 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 src/lib/__tests__/gateway-url.test.ts create mode 100644 src/lib/gateway-url.ts diff --git a/src/components/panels/multi-gateway-panel.tsx b/src/components/panels/multi-gateway-panel.tsx index b3a9bfa..9b06c9e 100644 --- a/src/components/panels/multi-gateway-panel.tsx +++ b/src/components/panels/multi-gateway-panel.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useCallback } from 'react' import { useMissionControl } from '@/store' import { useWebSocket } from '@/lib/websocket' +import { buildGatewayWebSocketUrl } from '@/lib/gateway-url' interface Gateway { id: number @@ -93,8 +94,11 @@ export function MultiGatewayPanel() { } const connectTo = (gw: Gateway) => { - const proto = window.location.protocol === 'https:' ? 'wss' : 'ws' - const wsUrl = `${proto}://${gw.host}:${gw.port}` + const wsUrl = buildGatewayWebSocketUrl({ + host: gw.host, + port: gw.port, + browserProtocol: window.location.protocol, + }) connect(wsUrl, '') // token is handled by the gateway entry, not passed to frontend } @@ -192,7 +196,7 @@ export function MultiGatewayPanel() { gateway={gw} health={healthByGatewayId.get(gw.id)} isProbing={probing === gw.id} - isCurrentlyConnected={connection.url?.includes(`:${gw.port}`) ?? false} + isCurrentlyConnected={(connection.url?.includes(gw.host) ?? false) || (connection.url?.includes(`:${gw.port}`) ?? false)} onSetPrimary={() => setPrimary(gw)} onDelete={() => deleteGateway(gw.id)} onConnect={() => connectTo(gw)} diff --git a/src/lib/__tests__/gateway-url.test.ts b/src/lib/__tests__/gateway-url.test.ts new file mode 100644 index 0000000..f6f1a43 --- /dev/null +++ b/src/lib/__tests__/gateway-url.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest' +import { buildGatewayWebSocketUrl } from '@/lib/gateway-url' + +describe('buildGatewayWebSocketUrl', () => { + it('builds ws URL with host and port for local dev', () => { + expect(buildGatewayWebSocketUrl({ + host: '127.0.0.1', + port: 18789, + browserProtocol: 'http:', + })).toBe('ws://127.0.0.1:18789') + }) + + it('omits 18789 for remote hosts on https browser context', () => { + expect(buildGatewayWebSocketUrl({ + host: 'cb-vcn.tail47c878.ts.net', + port: 18789, + browserProtocol: 'https:', + })).toBe('wss://cb-vcn.tail47c878.ts.net') + }) + + it('keeps explicit websocket URL host value unchanged aside from protocol normalization', () => { + expect(buildGatewayWebSocketUrl({ + host: 'https://gateway.example.com', + port: 18789, + browserProtocol: 'https:', + })).toBe('wss://gateway.example.com') + }) + + it('preserves explicit URL port when provided in host', () => { + expect(buildGatewayWebSocketUrl({ + host: 'https://gateway.example.com:8443', + port: 18789, + browserProtocol: 'https:', + })).toBe('wss://gateway.example.com:8443') + }) +}) diff --git a/src/lib/gateway-url.ts b/src/lib/gateway-url.ts new file mode 100644 index 0000000..60440d5 --- /dev/null +++ b/src/lib/gateway-url.ts @@ -0,0 +1,56 @@ +function isLocalHost(host: string): boolean { + const normalized = host.toLowerCase() + return ( + normalized === 'localhost' || + normalized === '127.0.0.1' || + normalized === '::1' || + normalized.endsWith('.local') + ) +} + +function normalizeProtocol(protocol: string): 'ws:' | 'wss:' { + if (protocol === 'https:' || protocol === 'wss:') return 'wss:' + return 'ws:' +} + +export function buildGatewayWebSocketUrl(input: { + host: string + port: number + browserProtocol?: string +}): string { + const rawHost = String(input.host || '').trim() + const port = Number(input.port) + const browserProtocol = input.browserProtocol === 'https:' ? 'https:' : 'http:' + + if (!rawHost) { + return `${browserProtocol === 'https:' ? 'wss' : 'ws'}://127.0.0.1:${port || 18789}` + } + + const prefixed = + rawHost.startsWith('ws://') || + rawHost.startsWith('wss://') || + rawHost.startsWith('http://') || + rawHost.startsWith('https://') + ? rawHost + : null + + if (prefixed) { + try { + const parsed = new URL(prefixed) + parsed.protocol = normalizeProtocol(parsed.protocol) + return parsed.toString().replace(/\/$/, '') + } catch { + return prefixed + } + } + + const wsProtocol = browserProtocol === 'https:' ? 'wss' : 'ws' + const shouldOmitPort = + wsProtocol === 'wss' && + !isLocalHost(rawHost) && + port === 18789 + + return shouldOmitPort + ? `${wsProtocol}://${rawHost}` + : `${wsProtocol}://${rawHost}:${port || 18789}` +} diff --git a/src/lib/websocket.ts b/src/lib/websocket.ts index e670222..99bbe30 100644 --- a/src/lib/websocket.ts +++ b/src/lib/websocket.ts @@ -83,6 +83,8 @@ export function useWebSocket() { normalized.includes('origin not allowed') || normalized.includes('device identity required') || normalized.includes('device_auth_signature_invalid') || + normalized.includes('invalid connect params') || + normalized.includes('/client/id') || normalized.includes('auth rate limit') || normalized.includes('rate limited') ) @@ -100,6 +102,9 @@ export function useWebSocket() { if (normalized.includes('device_auth_signature_invalid')) { return 'Gateway rejected device signature. Clear local device identity in the browser and reconnect.' } + if (normalized.includes('invalid connect params') || normalized.includes('/client/id')) { + return 'Gateway rejected client identity params. Ensure NEXT_PUBLIC_GATEWAY_CLIENT_ID is set to openclaw-control-ui and reconnect.' + } if (normalized.includes('auth rate limit') || normalized.includes('rate limited')) { return 'Gateway authentication is rate limited. Wait briefly, then reconnect.' }