diff --git a/src/App.tsx b/src/App.tsx index 03a8f52..593846b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,8 +32,10 @@ function App() { const join = async (appId: string) => { if (!appId) return; const res = await startSession({ appId }).unwrap(); - console.log("Joining debug session at " + res.url); - dispatch({ type: WS_CONNECT, payload: { url: res.url } }); + const primaryUrl = res.fallbackUrl || res.url; + const fallbackUrl = res.fallbackUrl ? res.url : undefined; + console.log("Joining debug session at " + primaryUrl + (fallbackUrl ? " (fallback: " + fallbackUrl + ")" : "")); + dispatch({ type: WS_CONNECT, payload: { url: primaryUrl, fallbackUrl } }); }; const stop = (appId: string) => { diff --git a/src/features/DebugConsole/DebugConsole.tsx b/src/features/DebugConsole/DebugConsole.tsx index 02f1332..86ad70f 100644 --- a/src/features/DebugConsole/DebugConsole.tsx +++ b/src/features/DebugConsole/DebugConsole.tsx @@ -25,10 +25,10 @@ const DebugConsole = ({isConnected, join, stop, clear}: DebugConsoleProps) => { const { appId } = useAppParams(); const dispatch = useAppDispatch(); const messages = useAppSelector((state: RootState) => state.websocket.messages); - const failedUrl = useAppSelector((state: RootState) => state.websocket.failedUrl); + const failedUrls = useAppSelector((state: RootState) => state.websocket.failedUrls); const searchText = useAppSelector(selectSearchText); - const certUrl = failedUrl - ? new URL(failedUrl).origin.replace(/^wss:/, 'https:').replace(/^ws:/, 'http:') + const certUrls = failedUrls + ? failedUrls.map((u: string) => new URL(u).origin.replace(/^wss:/, 'https:').replace(/^ws:/, 'http:')) : null; const { data: doNotLoadConfigOnNextBoot } = @@ -140,12 +140,17 @@ const DebugConsole = ({isConnected, join, stop, clear}: DebugConsoleProps) => { Message Count: {messages.length} - {certUrl && ( + {certUrls && certUrls.length > 0 && ( Connection failed. The debug server may have an untrusted certificate.{' '} - - Open {certUrl} in a new tab - + {certUrls.map((certUrl: string, i: number) => ( + + {i > 0 && ' or '} + + Open {certUrl} in a new tab + + + ))} {', accept the certificate, then try "Start Debug Session" again.'} )} diff --git a/src/store/apiSlice.ts b/src/store/apiSlice.ts index 3210514..3f6bbf8 100644 --- a/src/store/apiSlice.ts +++ b/src/store/apiSlice.ts @@ -388,6 +388,7 @@ interface MethodParam { interface DebugSession { url: string; + fallbackUrl?: string; } export interface MobileControlClient { diff --git a/src/store/websocketMiddleware.ts b/src/store/websocketMiddleware.ts index 6b60a7f..67dc18a 100644 --- a/src/store/websocketMiddleware.ts +++ b/src/store/websocketMiddleware.ts @@ -18,7 +18,7 @@ export const websocketMiddleware: Middleware = (store) => { const { type } = action as WsConnectAction | WsDisconnectAction; if (type === WS_CONNECT) { - const { url } = (action as WsConnectAction).payload; + const { url, fallbackUrl } = (action as WsConnectAction).payload; console.log("[ws] Connecting to", url); @@ -29,24 +29,37 @@ export const websocketMiddleware: Middleware = (store) => { socket.close(); } - socket = new WebSocket(url); - socket.onopen = () => store.dispatch(connected()); - socket.onclose = () => { - store.dispatch(disconnected()); - socket = null; - }; - socket.onerror = (err) => { - console.error("WebSocket error", err); - store.dispatch(connectionFailed(url)); - }; - socket.onmessage = (event: MessageEvent) => { - try { - store.dispatch(messageReceived(JSON.parse(event.data))); - } catch (e) { - console.error("Failed to parse WebSocket message", e); - } + const connectToUrl = (targetUrl: string, fallback?: string) => { + socket = new WebSocket(targetUrl); + socket.onopen = () => store.dispatch(connected()); + socket.onclose = () => { + store.dispatch(disconnected()); + socket = null; + }; + socket.onerror = (err) => { + console.error("WebSocket error", err); + if (fallback) { + console.log("[ws] Primary connection failed, falling back to", fallback); + connectToUrl(fallback); + } else { + // Report all attempted URLs (primary + fallback that was tried) + const attemptedUrls = fallbackUrl && targetUrl === fallbackUrl + ? [url, fallbackUrl] + : [targetUrl]; + store.dispatch(connectionFailed(attemptedUrls)); + } + }; + socket.onmessage = (event: MessageEvent) => { + try { + store.dispatch(messageReceived(JSON.parse(event.data))); + } catch (e) { + console.error("Failed to parse WebSocket message", e); + } + }; }; + connectToUrl(url, fallbackUrl); + return; } diff --git a/src/store/websocketSlice.ts b/src/store/websocketSlice.ts index a46b7b0..00c75c4 100644 --- a/src/store/websocketSlice.ts +++ b/src/store/websocketSlice.ts @@ -4,13 +4,13 @@ import { LogMessage } from "../shared/types/LogMessage"; interface WebsocketState { messages: LogMessage[]; isConnected: boolean; - failedUrl: string | null; + failedUrls: string[] | null; } const initialState: WebsocketState = { messages: [], isConnected: false, - failedUrl: null, + failedUrls: null, }; const websocketSlice = createSlice({ @@ -34,12 +34,12 @@ const websocketSlice = createSlice({ state.messages = []; }, /** Dispatched by the middleware when a connection attempt fails */ - connectionFailed(state, action: PayloadAction) { - state.failedUrl = action.payload; + connectionFailed(state, action: PayloadAction) { + state.failedUrls = action.payload; }, /** Dispatched by the middleware when a new connection attempt starts */ connectionAttemptStarted(state) { - state.failedUrl = null; + state.failedUrls = null; }, }, }); @@ -57,7 +57,7 @@ export const WS_DISCONNECT = "websocket/disconnect"; export interface WsConnectAction { type: typeof WS_CONNECT; - payload: { url: string }; + payload: { url: string; fallbackUrl?: string }; } export interface WsDisconnectAction {