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 {