fix(gateway): improve remote websocket URL handling and client-id errors
This commit is contained in:
parent
fbff5c7b14
commit
fce3b78706
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
@ -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}`
|
||||
}
|
||||
|
|
@ -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.'
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue