fix(gateway): improve remote websocket URL handling and client-id errors

This commit is contained in:
Nyk 2026-03-05 12:55:46 +07:00
parent fbff5c7b14
commit fce3b78706
4 changed files with 104 additions and 3 deletions

View File

@ -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)}

View File

@ -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')
})
})

56
src/lib/gateway-url.ts Normal file
View File

@ -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}`
}

View File

@ -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.'
}