provider.rpc_proxy
Documentation for eth_defi.provider.rpc_proxy Python module.
JSON-RPC failover proxy for multiple upstream RPC providers.
A lightweight threaded HTTP proxy that presents a single JSON-RPC endpoint while internally routing requests across multiple upstream RPC providers with automatic failover, retry, and per-provider statistics.
The primary use case is Anvil mainnet forks: Anvil accepts only a single
--fork-url and has no internal retry or failover logic. When the upstream
RPC is slow or rate-limited, Anvil hangs indefinitely. This proxy sits
between Anvil and the upstream RPCs, transparently handling failures.
However the proxy is general-purpose and can be used with any software that needs a single RPC URL backed by multiple upstreams.
Example usage:
from eth_defi.provider.rpc_proxy import start_rpc_proxy
proxy = start_rpc_proxy(
[
"https://rpc-provider-a.example.com",
"https://rpc-provider-b.example.com",
]
)
print(f"Proxy listening at {proxy.url}")
# Pass proxy.url to Anvil, or any other JSON-RPC client
# ...
# When done, shut down and see statistics
proxy.close()
See eth_defi.provider.anvil.launch_anvil() for automatic integration.
Functions
|
Default failure detection replicating |
|
Start a JSON-RPC failover proxy on a background thread. |
Classes
A running JSON-RPC failover proxy instance. |
|
Configuration for the JSON-RPC failover proxy. |
|
Per-provider statistics collected during proxy operation. |
- FailureHandler
Type alias for the failure detection callback.
Receives the HTTP status code and the parsed JSON-RPC response body (or
Noneif the body could not be parsed as JSON). ReturnsTrueif the response indicates a failure that should trigger a retry on the next provider.See
default_failure_handler()for the built-in implementation.
- class RPCProxyConfig
Bases:
objectConfiguration for the JSON-RPC failover proxy.
Collects all tuneable parameters of
start_rpc_proxy()into a single object with sensible defaults. Every field has a docstring explaining its purpose, default value, and interaction with other fields.You can construct this directly and pass it to
start_rpc_proxy(), or pass it as theproxy_multiple_upstreamargument tolaunch_anvil().Example — standalone proxy with custom configuration:
from eth_defi.provider.rpc_proxy import RPCProxyConfig, start_rpc_proxy config = RPCProxyConfig( timeout=15.0, retries=5, auto_switch_request_count=50, ) proxy = start_rpc_proxy( ["https://rpc-a.example.com", "https://rpc-b.example.com"], config=config, )Example — passing to
launch_anvil:from eth_defi.provider.anvil import launch_anvil from eth_defi.provider.rpc_proxy import RPCProxyConfig config = RPCProxyConfig(timeout=10.0, retries=4) launch = launch_anvil( fork_url="https://rpc-a.example.com https://rpc-b.example.com", proxy_multiple_upstream=config, )See also
RPCProxy,start_rpc_proxy(),default_failure_handler().- name: Optional[str]
Human-readable name for this proxy instance.
Used in log messages and as the background thread name to help identify which proxy is reporting when multiple proxies run concurrently (e.g. one per chain). If
None, a default name is generated from the port number.
- timeout: float
Per-upstream-attempt timeout in seconds.
Each individual request to an upstream RPC provider will be aborted after this duration. Set lower than the typical caller timeout (90 s) to leave room for failover attempts. For example, with the default of 30 s and 3 retries, the worst-case wall-clock time per incoming request is ~90 s — matching Anvil’s typical read timeout.
- retries: int
Maximum number of upstream attempts per incoming request.
The proxy cycles through available providers up to this many times before giving up and returning an
HTTP 502with a JSON-RPC error body to the caller. Each attempt targets the next provider in round-robin order (or the same provider if only one is configured).
- backoff: float
Initial sleep duration in seconds between retry attempts.
Grows by 1.5× after each retry (e.g. 0.5 → 0.75 → 1.125 …). Kept short because retries typically switch to a different provider, so there is no benefit in waiting for the same provider to recover.
- auto_switch_request_count: int
Number of successful requests to serve from one provider before automatically switching to the next in round-robin order.
Set to
0(the default) to disable auto-switching — the proxy will only switch providers on errors. Setting a positive value helps distribute load across providers and can detect degraded providers early by exercising all of them regularly.
- switchover_log_level: int
Logging level for upstream failure and switchover events.
Each time the proxy encounters a retryable error or switches to another upstream provider, it logs a message at this level. Defaults to
logging.INFOso failures are visible in normal operation. Set tologging.WARNINGorlogging.DEBUGto adjust verbosity.
- request_log_level: int
Logging level for request/response payload dumping.
When the effective logger level is at or below this threshold, the proxy logs the full JSON-RPC request body before forwarding and the full response body after receiving it. This is useful for debugging but generates significant output.
When the logger level is above this threshold the formatting is skipped entirely — zero overhead in production. Defaults to
logging.DEBUG.
- log_max_size: int
Maximum byte size for logged request/response payloads.
Payloads larger than this are truncated with a
"… (truncated, total N bytes)"suffix. Prevents massiveeth_getCodeordebug_traceTransactionresponses from flooding log files.
- pool_maxsize: int
Maximum number of connections per host in the HTTP pool.
Anvil can issue many concurrent requests during genesis fork creation, so a larger pool avoids
Connection pool is full, discarding connectionwarnings from urllib3.
- max_error_replies: int
Maximum number of error replies stored per provider in
UpstreamRPCProviderStatistics.error_replies.Oldest entries are discarded when this limit is reached, keeping memory bounded during long-running proxy sessions.
- failure_handler: Callable[[int, Optional[dict]], bool]
Custom failure detection callback.
Called with
(http_status, parsed_json_body)for every upstream response. Must returnTrueif the response should be treated as a retryable failure (triggering a switch to the next provider).Connection-level failures (timeouts, refused connections) bypass this handler and are always retried.
Defaults to
default_failure_handler(), which replicates the battle-tested error classification frometh_defi.middleware.
- suppress_client_disconnect_errors: bool
Suppress downstream client disconnect tracebacks.
Used by Anvil fork tests where Anvil can close in-flight proxy requests during process shutdown. Direct proxy users keep the default strict behaviour so unexpected client disconnects are still visible.
- describe()
Return a human-readable summary of the configuration for logging.
Includes all tuneable numeric fields for full visibility.
Example output:
timeout=30.0, retries=3, backoff=0.5, auto_switch_request_count=0, pool_maxsize=50, max_error_replies=100, log_max_size=2048
- Return type
- __init__(name=None, timeout=30.0, retries=3, backoff=0.5, auto_switch_request_count=0, switchover_log_level=20, request_log_level=10, log_max_size=2048, pool_maxsize=50, max_error_replies=100, failure_handler=None, suppress_client_disconnect_errors=False)
- default_failure_handler(http_status, json_body)
Default failure detection replicating
eth_defi.middlewarelogic.Adapted from
eth_defi.middleware.is_retryable_http_exception()andeth_defi.provider.fallback.FallbackProviderto work at the HTTP proxy level with raw status codes and JSON bodies rather than Python exceptions.The checks are, in order:
HTTP status code against
eth_defi.middleware.DEFAULT_RETRYABLE_HTTP_STATUS_CODESJSON-RPC
error.codeagainsteth_defi.middleware.DEFAULT_RETRYABLE_RPC_ERROR_CODESJSON-RPC
error.messagesubstring match againsteth_defi.middleware.DEFAULT_RETRYABLE_RPC_ERROR_MESSAGES
Connection-level failures (timeouts, refused connections) are always retried and never reach this handler — they are caught earlier in
_ProxyRequestHandler._try_upstream().
- class UpstreamRPCProviderStatistics
Bases:
objectPer-provider statistics collected during proxy operation.
Tracks request counts, failure counts, and method-level breakdowns for each upstream RPC provider. Instances are keyed by provider URL in
RPCProxy.provider_stats.- last_failure: Optional[datetime.datetime]
Timestamp of the last failure (naive UTC), or
Noneif no failures
- method_counts: dict[str, int]
Breakdown of requests by JSON-RPC method name. e.g.
{"eth_getBlockByNumber": 42, "eth_getBalance": 15}
- error_replies: list[dict]
List of error replies received from this provider. Each entry is a dict with keys:
"timestamp"(naive UTC),"method"(JSON-RPC method),"http_status"(int or None),"error"(str summary — exception message, HTTP status text, or JSON-RPC error message). Capped atDEFAULT_MAX_ERROR_REPLIESto avoid unbounded growth.
- record_request(method)
Record a request being sent to this provider.
- Parameters
method (str) –
- Return type
None
- record_failure(method, error_summary, http_status=None, max_error_replies=100)
Record a failed request to this provider.
- __init__(url, request_count=0, failure_count=0, last_failure=None, method_counts=<factory>, method_failure_counts=<factory>, error_replies=<factory>, _lock=<factory>)
- class RPCProxy
Bases:
objectA running JSON-RPC failover proxy instance.
Manages the lifecycle of a background threaded HTTP server that presents a single
http://127.0.0.1:{port}endpoint while internally routing JSON-RPC requests across multiple upstream RPC providers with automatic failover, retry, and per-provider statistics collection.Why this exists
Anvil (and other tools) accept only a single
--fork-urland have no internal retry or failover logic. When the upstream RPC is slow, rate-limited, or temporarily unreachable, Anvil hangs indefinitely — causing downstream callers to timeout (e.g.eth_getTransactionCounttiming out after 90 s).This proxy sits between the consumer and multiple upstream RPCs. If one upstream fails, the proxy transparently switches to the next one and retries, all within a configurable timeout budget. On shutdown it logs per-provider statistics so you can identify flaky or slow providers.
How it works
start_rpc_proxy()allocates a free localhost port and starts aHTTPServer(withThreadingMixIn) on a daemon thread.Every incoming
POSTis forwarded to the currently-active upstream. If the upstream returns a retryable error (as determined by theFailureHandler), the proxy switches to the next upstream and retries — up toDEFAULT_RETRIEStimes.Connection-level failures (timeouts, refused connections) are always retried without consulting the failure handler.
After all retries are exhausted the proxy returns an
HTTP 502with a JSON-RPC error body so the caller can distinguish proxy-level failures from upstream errors.Calling
close()stops the server and logs a per-provider summary tologger.info().
Lifecycle
Created by
start_rpc_proxy(). The proxy runs untilclose()is called. When used withlaunch_anvil(), the lifecycle is automatic: the proxy starts before Anvil and shuts down whenclose()is called.Standalone usage
from eth_defi.provider.rpc_proxy import start_rpc_proxy # Start a proxy backed by two upstream RPCs proxy = start_rpc_proxy( [ "https://eth-mainnet.alchemyapi.io/v2/YOUR_KEY", "https://rpc.ankr.com/eth", ] ) # proxy.url is e.g. "http://127.0.0.1:23456" # Pass it to any tool that accepts a single JSON-RPC URL: # anvil --fork-url {proxy.url} # curl -X POST {proxy.url} -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' # Inspect statistics at any time for name, stats in proxy.get_stats().items(): print(f"{name}: {stats.request_count} requests, {stats.failure_count} failures") # Shut down and see final statistics in the log proxy.close()
With custom parameters
import logging from eth_defi.provider.rpc_proxy import start_rpc_proxy proxy = start_rpc_proxy( rpc_urls=[ "https://rpc-provider-a.example.com", "https://rpc-provider-b.example.com", ], timeout=15.0, # 15 s per upstream attempt retries=5, # try up to 5 times auto_switch_request_count=100, # rotate providers every 100 requests switchover_log_level=logging.WARNING, request_log_level=logging.DEBUG, # dump payloads at DEBUG level log_max_size=4096, # truncate large payloads at 4 KiB )
Automatic integration with Anvil
When
launch_anvil()receives a space-separatedfork_urlcontaining multiple RPC endpoints, it automatically starts anRPCProxyand passesproxy.urlas Anvil’s--fork-url. The proxy’s lifecycle is tied toclose():from eth_defi.provider.anvil import launch_anvil # Space-separated URLs trigger the proxy automatically launch = launch_anvil( fork_url="https://rpc-a.example.com https://rpc-b.example.com", ) # launch.proxy is the RPCProxy instance # ...run your test... launch.close() # stops Anvil, then stops the proxy and logs stats
You can also pass an
RPCProxyyou created yourself, or anRPCProxyConfigto fine-tune settings, via theproxy_multiple_upstreamparameter — seelaunch_anvil()for details.See also
RPCProxyConfig,start_rpc_proxy(),UpstreamRPCProviderStatistics.- provider_stats: dict[str, eth_defi.provider.rpc_proxy.UpstreamRPCProviderStatistics]
Per-provider statistics, keyed by provider display name (URL with API keys stripped) for easy lookup.
- close()
Shut down the proxy server and log final statistics.
Stops accepting new requests, waits for the server thread to finish, then logs a summary of per-provider statistics at
INFOlevel.- Return type
None
- get_stats()
Return per-provider statistics, keyed by display URL.
- Returns
Dictionary mapping provider display name to its statistics.
- Return type
dict[str, eth_defi.provider.rpc_proxy.UpstreamRPCProviderStatistics]
- __init__(name, port, url, provider_stats, _server_thread, _http_server)
- Parameters
name (str) –
port (int) –
url (str) –
provider_stats (dict[str, eth_defi.provider.rpc_proxy.UpstreamRPCProviderStatistics]) –
_server_thread (threading.Thread) –
_http_server (eth_defi.provider.rpc_proxy._ThreadingHTTPServer) –
- Return type
None
- start_rpc_proxy(rpc_urls, port=None, config=None, **kwargs)
Start a JSON-RPC failover proxy on a background thread.
The proxy listens on
localhostand forwards incoming JSON-RPC POST requests to the given upstream RPC URLs with automatic failover, retry, and statistics collection.- Parameters
rpc_urls (list[str]) – Upstream RPC endpoint URLs to cycle through. At least one URL is required.
Local port to bind on
127.0.0.1.If
None(the default), the server binds to port0which tells the operating system to assign a free ephemeral port atomically. The actual port is read back from the socket after binding and stored inRPCProxy.port. This avoids the TOCTOU race condition that occurs withfind_free_port(): that function checks availability viaconnect(), but under heavy parallel test execution (pytest -n auto) another process can grab the same port between the check and thebind()call, resulting inOSError: [Errno 98] Address already in use.Pass an explicit port number only when you need a deterministic address (e.g. for debugging or firewall rules).
config (Optional[eth_defi.provider.rpc_proxy.RPCProxyConfig]) – Proxy configuration. If
None, a defaultRPCProxyConfigis used. Individual fields can be overridden via**kwargs.kwargs – Override individual
RPCProxyConfigfields. For examplestart_rpc_proxy(urls, timeout=10.0)is equivalent tostart_rpc_proxy(urls, config=RPCProxyConfig(timeout=10.0)). When bothconfigandkwargsare provided,kwargswin.
- Returns
A running
RPCProxyinstance. CallRPCProxy.close()when done.- Return type