Skip to content

fix(evm): fail over on Chrome network errors instead of claiming the vault is down - #134

Merged
BitHighlander merged 1 commit into
developfrom
fix/rpc-failover-not-vault-offline
Aug 15, 2026
Merged

fix(evm): fail over on Chrome network errors instead of claiming the vault is down#134
BitHighlander merged 1 commit into
developfrom
fix/rpc-failover-not-vault-offline

Conversation

@BitHighlander

Copy link
Copy Markdown
Collaborator

The bug

A dApp eth_sendTransaction failed with:

KeepKey Vault is not running. Open the KeepKey Vault desktop app, then try again.

The vault was running the whole time. Two message-text classifiers combine to turn an Ethereum RPC that won't connect into a false claim that the vault is offline:

isTransientRpcError("Failed to fetch")     -> false   => hard-throw, failover loop aborts
isVaultUnreachableError("Failed to fetch")  -> true    => "Vault is not running"

Chrome throws byte-identical text for an unreachable RPC and a closed vault. No regex can separate them.

Concretely: ethereum-rpc.publicnode.com is first in lastResortRpcs['eip155:1'] and currently fails at TLS from some networks (curl exit 35). withRpcFailover classified that as definitive, aborted on the first URL, and never reached eth.drpc.org — which works fine. The resulting error hit the catch-all in methods.ts and got relabeled as a vault problem.

Not a regression: isTransientRpcError was written against Firefox/Node wording (NetworkError when attempting to fetch resource), so the same dead RPC failed over on Firefox and hard-failed on Chrome. Latent since it was introduced; surfaced now because a first-in-list RPC started failing at connection level.

Ruled out with evidence before touching anything: Pioneer discovery returns 8 mainnet RPCs in 0.13s (6 of 8 answer eth_chainId), and the vault answers /api/health healthy with correct CORS including for a chrome-extension:// origin.

Changes

  • isTransientRpcError learns browser wordingfailed to fetch, load failed, err_ (Chrome ERR_* net errors), aborted. The failover loop now tries the remaining URLs instead of dying on the first bad one.
  • Two drifted copies collapsed into one export. ethereumHandler.ts had its own copy with method-rejection patterns that rpcFailover.ts lacked; it now imports the shared classifier.
  • Both definitive branches log the URL before throwing. They were silent while the transient branch right below them logged properly — that asymmetry is the single biggest reason this looked like a vault bug, because the failing URL never appeared in the console.
  • formatUserError asks the vault instead of guessing. It now probes localhost:1646 before claiming the vault is down. Cheap (localhost, only on an already-failed request) and authoritative, unlike inferring vault state from an arbitrary error string in a catch-all.

Tests

128 passed, type-check and lint clean (0 errors).

  • rpcFailover.test.ts — the browser connection-error strings, plus the method-rejection patterns that previously had no coverage in this module.
  • utils.test.ts — an RPC-origin Failed to fetch must not produce VAULT_REQUIRED_MESSAGE when the vault answers. This is the regression guard.

Verified manually

Built and loaded unpacked; dApp EVM send succeeds, console shows the failover from publicnode.com to eth.drpc.org. Vault-closed path re-checked separately and still shows the launch instruction.

Deliberately not done

Reordering lastResortRpcs['eip155:1'] to put eth.drpc.org first. One machine's TLS failure isn't grounds for changing a global default, and failover now covers it.

🤖 Generated with Claude Code

…vault is down

A dApp eth_sendTransaction failed with "KeepKey Vault is not running" while
the vault was running. Two message-text classifiers combined to turn a dead
Ethereum RPC into a false claim about the vault:

  isTransientRpcError("Failed to fetch")     -> false  => failover aborts
  isVaultUnreachableError("Failed to fetch")  -> true   => "Vault not running"

Chrome throws byte-identical text for an unreachable RPC and a closed vault,
so no regex can separate them.

- isTransientRpcError now covers browser connection-level wording
  (failed to fetch / load failed / err_ / aborted). It was written against
  Firefox/Node wording, so the same dead RPC failed over on Firefox and
  hard-threw on Chrome. The loop now tries the remaining URLs.
- Collapse the two copies of the classifier (ethereumHandler + rpcFailover)
  into one export; they had already drifted.
- Log the URL before both definitive throws. That branch was silent while the
  transient branch logged, so the failing RPC never appeared in the console —
  the single biggest reason this was misdiagnosed as a vault problem.
- formatUserError probes localhost:1646 before blaming the vault, instead of
  inferring vault state from an arbitrary error string in a catch-all.

Tests: browser error strings in rpcFailover.test.ts; utils.test.ts asserts an
RPC-origin "Failed to fetch" does NOT produce VAULT_REQUIRED_MESSAGE when the
vault answers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@BitHighlander
BitHighlander merged commit b17f979 into develop Aug 15, 2026
7 checks passed
@BitHighlander
BitHighlander deleted the fix/rpc-failover-not-vault-offline branch August 15, 2026 04:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant