diff --git a/src/shared/utils/wsPath.ts b/src/shared/utils/wsPath.ts index 84cd08f445e9..dfbfba5bc604 100644 --- a/src/shared/utils/wsPath.ts +++ b/src/shared/utils/wsPath.ts @@ -53,3 +53,61 @@ export function resolveLiveWsPublicUrl(env: NodeJS.ProcessEnv = process.env): st export function getLiveWsPath(): string { return deriveLiveWsPath(resolveLiveWsPublicUrl() ?? undefined); } + +/** A port the handshake may report, or null when it is not usable. */ +export function sanitizeLiveWsPort(port: unknown): number | null { + const value = typeof port === "string" ? Number(port) : port; + if (typeof value !== "number" || !Number.isInteger(value)) return null; + return value > 0 && value < 65536 ? value : null; +} + +export interface LiveWsUrlParts { + /** Explicit `wsUrl` passed by the caller - always wins. */ + explicit?: string | null; + /** `live.publicUrl` from the handshake - a complete URL, used as-is. */ + handshakeUrl?: string | null; + /** `live.port` from the handshake, i.e. the running LIVE_WS_PORT. */ + handshakePort?: number | null; + /** `live.path` from the handshake. */ + handshakePath?: string | null; + /** The compiled-in default, used for everything the handshake does not say. */ + defaultUrl: string; +} + +/** + * Resolve the live dashboard WebSocket URL. + * + * The handshake reports the port the live server is actually listening on, but + * the client read only `publicUrl` and `path` from it. An operator who moved + * the server with `LIVE_WS_PORT` still got the compiled-in 20132, and the + * dashboard sat on "Live disabled - WebSocket disconnected" with no way to + * correct it short of rebuilding the image (#11331). + * + * Precedence: an explicit `wsUrl` wins, then a complete `publicUrl` from the + * handshake, then the default URL with whatever port and path the handshake + * reported applied to it. + */ +export function resolveLiveWsUrl({ + explicit, + handshakeUrl, + handshakePort, + handshakePath, + defaultUrl, +}: LiveWsUrlParts): string { + if (explicit) return explicit; + if (handshakeUrl) return handshakeUrl; + + const port = sanitizeLiveWsPort(handshakePort); + const path = + typeof handshakePath === "string" && handshakePath.startsWith("/") ? handshakePath : null; + if (port === null && path === null) return defaultUrl; + + try { + const url = new URL(defaultUrl); + if (port !== null) url.port = String(port); + if (path !== null) url.pathname = path; + return url.toString(); + } catch { + return defaultUrl; + } +}