diff --git a/packages/network-controller/tests/NetworkController.provider.test.ts b/packages/network-controller/tests/NetworkController.provider.test.ts index 2819f0de7f6..b315a00dada 100644 --- a/packages/network-controller/tests/NetworkController.provider.test.ts +++ b/packages/network-controller/tests/NetworkController.provider.test.ts @@ -626,6 +626,105 @@ describe('NetworkController provider tests', () => { ); }); + it('does not fail over when the selected RPC endpoint of a network is custom, even if failover URLs are configured and failover is enabled', async () => { + const customEndpointUrl = 'https://custom.endpoint'; + const failoverEndpointUrl = 'https://failover.endpoint'; + const networkClientId = 'custom-network-client-id'; + const rpcMethod = 'eth_gasPrice'; + + // The selected (custom) endpoint always errors. + nock(customEndpointUrl) + .post('/', { + id: /^\d+$/u, + jsonrpc: '2.0', + method: 'eth_blockNumber', + params: [], + }) + .times(15) + .reply(503); + // The failover endpoint would happily serve requests. If failover were + // (wrongly) honored for a custom endpoint, the request would divert here + // and succeed instead of throwing. We assert below that it is never hit. + const failoverScope = nock(failoverEndpointUrl) + .post('/', { + id: /^\d+$/u, + jsonrpc: '2.0', + method: 'eth_blockNumber', + params: [], + }) + .reply(200, { + id: 1, + jsonrpc: '2.0', + result: '0x1', + }) + .post('/', { + id: 1, + jsonrpc: '2.0', + method: rpcMethod, + params: [], + }) + .reply(200, { + id: 1, + jsonrpc: '2.0', + result: 'ok', + }); + + await withController( + { + isRpcFailoverEnabled: true, + state: { + networkConfigurationsByChainId: { + // The network offers both an Infura and a custom endpoint, with the + // custom one selected. + '0x1': buildInfuraNetworkConfiguration(InfuraNetworkType.mainnet, { + rpcEndpoints: [ + buildInfuraRpcEndpoint(InfuraNetworkType.mainnet), + buildCustomRpcEndpoint({ + networkClientId, + url: customEndpointUrl, + failoverUrls: [failoverEndpointUrl], + }), + ], + defaultRpcEndpointIndex: 1, + }), + }, + networksMetadata: { + [networkClientId]: { + EIPS: {}, + status: NetworkStatus.Unknown, + }, + }, + selectedNetworkClientId: networkClientId, + }, + }, + async ({ controller, messenger }) => { + messenger.subscribe('NetworkController:rpcEndpointRetried', () => { + jest.advanceTimersToNextTimer(); + }); + const { provider } = controller.getNetworkClientById(networkClientId); + const request = { + id: 1, + jsonrpc: '2.0' as const, + method: rpcMethod, + params: [], + }; + const expectedError = 'RPC endpoint not found or unavailable'; + + // Hit the primary, run out of retries. + await expect(provider.request(request)).rejects.toThrow(expectedError); + // Hit the primary, run out of retries. + await expect(provider.request(request)).rejects.toThrow(expectedError); + // Hit the primary, break the circuit. Since failover is not honored for + // a custom endpoint, there is nowhere to divert to, so this still + // throws rather than succeeding via the failover endpoint. + await expect(provider.request(request)).rejects.toThrow(expectedError); + + // The failover endpoint was never contacted. + expect(failoverScope.isDone()).toBe(false); + }, + ); + }); + it('transitions the status of a network client from "unavailable" to "available" when its (sole) RPC endpoint consistently returns 5xx errors for a while and then recovers', async () => { const endpointUrl = 'https://some.endpoint'; const networkClientId = 'AAAA-AAAA-AAAA-AAAA';