Skip to content

Feat: RPC failover support(read only) - #1073

Draft
tomatoishealthy wants to merge 2 commits into
mainfrom
feat/l1-rpc-failover-support
Draft

tomatoishealthy wants to merge 2 commits into
mainfrom
feat/l1-rpc-failover-support

Conversation

@tomatoishealthy

Copy link
Copy Markdown
Contributor

No description provided.

allen.wu and others added 2 commits September 21, 2026 16:05
--l1.rpc now accepts a comma-separated endpoint list, primary first. Failover
lives in an http.RoundTripper below the rpc layer, so every existing consumer
keeps taking a plain *ethclient.Client and the rollup binding still receives a
full bind.ContractBackend, including the SubscribeFilterLogs method that an
HTTP-only client could not honestly implement.

An endpoint is judged failed on a transport error, a 408/429/5xx status, or a
reply that is not JSON. The last rule is not theoretical: on a network whose
resolver answers for names that do not exist, a vanished endpoint returns HTTP
200 with an HTML block page. Judged on status alone that reads as success, and a
devnet run showed the node keep using the dead endpoint until every L1 read
failed upstream with "invalid character '<'", silently stopping block production
with no error and no restart.

Each attempt also carries GetBody so net/http can re-send a request whose pooled
connection died. Without it that routine keep-alive race surfaces as an endpoint
failure and the sticky switch demotes a healthy primary for the life of the
process.

A single endpoint keeps the previous ethclient.Dial behaviour, including ws://
and IPC, so existing configurations are unaffected.

AI-Developer: claude-opus-4.8
AI-Agent: claude code
AI-Reviewer: none
Harness-Skill: none
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A round of review findings on node/rpcfailover, plus failback to the primary.

Endpoint health. 401 and 403 now count as endpoint failures: endpoints carry
their own credentials, so an expired key or an exhausted quota on one provider
says nothing about the next, and hosted providers use exactly these statuses for
it. 3xx counts too, because attempt() rewrites every request's URL: a redirect
target would be discarded and the same endpoint asked again until http.Client
gives up after ten hops, with no failover. 400 and 404 still pass through, since
those describe the request rather than the endpoint.

Credential redaction. Two paths leaked an API key held in the URL path or query,
because net/http's stripPassword only masks userinfo. One is url.Parse's error,
which formats as parse "<raw url>". The other is the bootstrap URL given to
rpc.DialOptions, which is also the URL http.Client reports when it wraps a
transport error; that URL is only a default target which attempt() always
replaces, so redacting it costs nothing.

Response body bound. A server that sends headers and then goes quiet had no bound
at all: ResponseHeaderTimeout stops at the headers, the rpc layer sets no
deadline, and the node's callers pass contexts derived from context.Background().
The caller blocked forever, and since L1Tracker calls the RPC inline from its tick
loop and evaluates the halt gate inside that same loop, the gate stayed open --
the node would keep producing against an arbitrarily stale L1 view with nothing
to report it. An inactivity deadline on the connection bounds this without
capping legitimately large replies.

HTTP/2 is disabled because that bound depends on it. The deadline lives on the
connection, and under HTTP/2 one connection carries many concurrent streams whose
traffic would keep pushing it forward. TLSNextProto alone is not enough: it stops
net/http from handing the connection to its HTTP/2 implementation but not ALPN
from negotiating h2, and a server that then selects h2 leaves the HTTP/1.x code
reading frames as a response.

Failback. A fallback is no longer permanent -- every failbackAfter, one call
re-tries the primary, so a recovered primary is re-adopted instead of waiting for
the fallback to fail in turn. No background goroutine and no probe traffic; the
window is advanced by CompareAndSwap, so concurrent calls share one extra attempt
per window rather than paying one each.

Each attempt now carries GetBody, so net/http can re-send a request whose pooled
connection died. Without it that routine keep-alive race surfaced as an endpoint
failure and the sticky switch demoted a healthy primary for the life of the
process.

Known gaps are unchanged and documented in the package comment: a body that
stalls after its first bytes is bounded but cannot be failed over, and a gateway
reply that is JSON without being a JSON-RPC envelope is not detected.

AI-Developer: claude-opus-4.8
AI-Agent: claude code
AI-Reviewer: none
Harness-Skill: none
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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