From db9bc0259e21406321f15c39326fd827cdce4ae8 Mon Sep 17 00:00:00 2001 From: Amir Ekbatanifard Date: Mon, 10 Nov 2025 10:16:36 +0330 Subject: [PATCH 1/2] refactor: implement fallback API connection --- .../src/Popup/contexts/ApiProvider.tsx | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/packages/extension-ui/src/Popup/contexts/ApiProvider.tsx b/packages/extension-ui/src/Popup/contexts/ApiProvider.tsx index e9cf138361..dca0412dcc 100644 --- a/packages/extension-ui/src/Popup/contexts/ApiProvider.tsx +++ b/packages/extension-ui/src/Popup/contexts/ApiProvider.tsx @@ -159,15 +159,31 @@ export default function ApiProvider ({ children }: { children: React.ReactNode } }, [handleNewApi, updateEndpoint, resolvePendingConnections]); const connectToEndpoint = useCallback(async (genesisHash: string, endpointToConnect: string) => { + let wsProvider; + try { - const wsProvider = new WsProvider(endpointToConnect); - const newApi = await ApiPromise.create({ provider: wsProvider }); + wsProvider = new WsProvider(endpointToConnect); + + const newApi = await ApiPromise.create({ provider: wsProvider, throwOnConnect: true }); // throwOnConnect will throw error if the connection failed handleNewApi(newApi, endpointToConnect); } catch (error) { console.error('Connection error:', error); + await wsProvider?.disconnect(); + // Resolve pending with undefined on error resolvePendingConnections(genesisHash, undefined, endpointToConnect); + + let endpoint = endpointManager.get(genesisHash); + + // Throw an error only when the endpoint is in auto mode, to trigger a fallback API connection. + if (endpoint?.isAuto) { + endpoint = { ...AUTO_MODE_DEFAULT_ENDPOINT, timestamp: Date.now() }; + + endpointManager.set(genesisHash, endpoint); + + throw error; + } } }, [handleNewApi, resolvePendingConnections]); @@ -183,7 +199,7 @@ export default function ApiProvider ({ children }: { children: React.ReactNode } } }, [handleNewApi, resolvePendingConnections]); - const requestApiConnection = useCallback((genesisHash: string, endpoint: EndpointType | undefined, endpoints: DropdownOption[]) => { + const requestApiConnection = useCallback(async (genesisHash: string, endpoint: EndpointType | undefined, endpoints: DropdownOption[]) => { const endpointValue = endpoint?.endpoint; if (!endpointValue || !endpointManager) { @@ -201,19 +217,17 @@ export default function ApiProvider ({ children }: { children: React.ReactNode } // If in auto mode find the fastest endpoint if (isAutoMode(endpointValue)) { - handleAutoMode(genesisHash, endpoints).catch(console.error); - - return; + await handleAutoMode(genesisHash, endpoints); } // Connect to a WebSocket endpoint if (endpointValue.startsWith('wss')) { - connectToEndpoint(genesisHash, endpointValue).catch(console.error); + await connectToEndpoint(genesisHash, endpointValue); } // Connect to a light client endpoint if provided if (endpointValue.startsWith('light')) { - connectToLightClient(genesisHash, endpointValue).catch(console.error); + await connectToLightClient(genesisHash, endpointValue); } }, [connectToEndpoint, connectToLightClient, handleAutoMode]); @@ -266,10 +280,17 @@ export default function ApiProvider ({ children }: { children: React.ReactNode } resolve: resolvePromise }; - // Start connection - requestApiConnection(genesisHash, endpoint, endpoints); + try { + // Start connection + await requestApiConnection(genesisHash, endpoint, endpoints); - return promise; + return promise; + } catch (error) { + console.error(`Connection failed for ${endpointValue}`, error); + + // Retry with auto mode + return getApi(genesisHash, endpoints); + } }, [requestApiConnection]); return ( From d81910cd994d82c190ac3b1dfd7121f3df5ff8e9 Mon Sep 17 00:00:00 2001 From: Amir Ekbatanifard Date: Tue, 11 Nov 2025 10:34:23 +0330 Subject: [PATCH 2/2] refactor: limit API reconnection attempts to prevent infinite retries --- .../src/Popup/contexts/ApiProvider.tsx | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/extension-ui/src/Popup/contexts/ApiProvider.tsx b/packages/extension-ui/src/Popup/contexts/ApiProvider.tsx index dca0412dcc..a7b59a5ef1 100644 --- a/packages/extension-ui/src/Popup/contexts/ApiProvider.tsx +++ b/packages/extension-ui/src/Popup/contexts/ApiProvider.tsx @@ -13,7 +13,7 @@ import LCConnector from '@polkadot/extension-polkagate/src/util/api/lightClient- import { AUTO_MODE, AUTO_MODE_DEFAULT_ENDPOINT } from '@polkadot/extension-polkagate/src/util/constants'; const isAutoMode = (e: string) => e === AUTO_MODE.value; - +const MAX_RETRIES = 3; const endpointManager = new EndpointManager(); /** @@ -169,7 +169,7 @@ export default function ApiProvider ({ children }: { children: React.ReactNode } handleNewApi(newApi, endpointToConnect); } catch (error) { console.error('Connection error:', error); - await wsProvider?.disconnect(); + wsProvider?.disconnect().catch(console.error); // Resolve pending with undefined on error resolvePendingConnections(genesisHash, undefined, endpointToConnect); @@ -218,6 +218,8 @@ export default function ApiProvider ({ children }: { children: React.ReactNode } // If in auto mode find the fastest endpoint if (isAutoMode(endpointValue)) { await handleAutoMode(genesisHash, endpoints); + + return; } // Connect to a WebSocket endpoint @@ -231,7 +233,7 @@ export default function ApiProvider ({ children }: { children: React.ReactNode } } }, [connectToEndpoint, connectToLightClient, handleAutoMode]); - const getApi = useCallback(async (genesisHash: string | null | undefined, endpoints: DropdownOption[]): Promise => { + const getApi = useCallback(async (genesisHash: string | null | undefined, endpoints: DropdownOption[], retryCount = 0): Promise => { if (!genesisHash) { return Promise.resolve(undefined); } @@ -288,8 +290,22 @@ export default function ApiProvider ({ children }: { children: React.ReactNode } } catch (error) { console.error(`Connection failed for ${endpointValue}`, error); - // Retry with auto mode - return getApi(genesisHash, endpoints); + // Retry with exponential backoff + if (retryCount < MAX_RETRIES) { + const delay = 500 * Math.pow(2, retryCount); // 500ms, 1000ms, 2000ms... + + console.warn( + `Retrying connection for ${endpointValue} (attempt ${retryCount + 1}/${MAX_RETRIES}) after ${delay}ms` + ); + + await new Promise((resolve) => setTimeout(resolve, delay)); + + return getApi(genesisHash, endpoints, retryCount + 1); + } + + console.error(`❌ Max retry attempts reached for ${endpointValue}`); + + return Promise.resolve(undefined); } }, [requestApiConnection]);