@@ -258,19 +258,29 @@ def __init__(
258258 if not rpc_urls :
259259 raise ValueError ("RotatingHTTPProvider requires at least one RPC URL." )
260260
261- # Initialize the parent HTTPProvider with the first URL. The parent's
262- # session/transport machinery is unused — every request is delegated
263- # to one of ``self._providers`` — but ``isinstance(self, HTTPProvider)``
264- # must hold so web3 treats us as a proper provider (verified against
265- # web3.py 6.x; the parent does not open any connections eagerly).
261+ # Set the attributes the ``endpoint_uri`` getter reads *before*
262+ # calling ``super().__init__``. The parent constructor writes to
263+ # ``self.endpoint_uri`` (which our no-op setter absorbs); some web3
264+ # code paths read ``endpoint_uri`` from within the parent init —
265+ # hoisting these assignments keeps the getter defensible regardless
266+ # of which parent code paths are taken on a future web3 upgrade.
267+ # When upgrading web3.py: re-verify ``HTTPProvider.__init__`` still
268+ # does not read ``self.endpoint_uri`` after assignment, and that
269+ # the rotating provider is never constructed with ``session=...``
270+ # passed through (parent reads ``endpoint_uri`` in that branch).
271+ self ._rpc_urls : List [str ] = rpc_urls
272+ self ._current_index : int = 0
273+
274+ # ``isinstance(self, HTTPProvider)`` must hold so web3 treats us
275+ # as a proper provider; the parent's session/transport machinery
276+ # is otherwise unused since every request is delegated to one of
277+ # ``self._providers``.
266278 super ().__init__ (endpoint_uri = rpc_urls [0 ], request_kwargs = request_kwargs )
267279
268- self ._rpc_urls : List [str ] = rpc_urls
269280 self ._providers : List [HTTPProvider ] = [
270281 HTTPProvider (endpoint_uri = url , request_kwargs = request_kwargs )
271282 for url in rpc_urls
272283 ]
273- self ._current_index : int = 0
274284 self ._backoff_until : Dict [int , float ] = {}
275285 # Reentrant so health-tracking helpers can be called both directly
276286 # (from ``_handle_error_and_rotate``) and from inside ``_rotate``,
@@ -319,14 +329,22 @@ def endpoint_uri(self) -> str: # type: ignore[override]
319329 return self ._rpc_urls [self ._current_index ]
320330
321331 @endpoint_uri .setter
322- def endpoint_uri (self , _value : str ) -> None :
332+ def endpoint_uri (self , value : str ) -> None :
323333 """No-op setter retained for parent-class compatibility.
324334
325- :param _value : ignored. ``HTTPProvider.__init__`` assigns to
335+ :param value : ignored. ``HTTPProvider.__init__`` assigns to
326336 ``endpoint_uri`` once at construction; we accept the write so the
327337 parent constructor does not raise, but the active endpoint is
328338 always derived from ``self._rpc_urls[self._current_index]``.
329339 """
340+ # Surface the no-op so a future caller doing
341+ # ``provider.endpoint_uri = "https://override"`` can see at DEBUG
342+ # level that the assignment did not change the active endpoint.
343+ _logger .debug (
344+ "RotatingHTTPProvider ignores endpoint_uri assignment to %r; "
345+ "active URL is derived from self._rpc_urls[self._current_index]" ,
346+ value ,
347+ )
330348
331349 # ------------------------------------------------------------------
332350 # Per-RPC health tracking
@@ -365,7 +383,15 @@ def _rotate(self) -> bool:
365383 return False
366384
367385 now = time .monotonic ()
368- if now - self ._last_rotation_time < ROTATION_COOLDOWN :
386+ # The cooldown prevents thrashing between *healthy* providers
387+ # (e.g. concurrent threads each triggering a rotation). When
388+ # the current provider is itself in backoff — which is the
389+ # case immediately after ``_handle_error_and_rotate`` marks
390+ # it unhealthy — refusing to rotate would just loop the
391+ # next attempt back to a known-bad endpoint and burn the
392+ # per-call retry budget. Skip the cooldown in that case.
393+ current_healthy = now >= self ._backoff_until .get (self ._current_index , 0.0 )
394+ if current_healthy and now - self ._last_rotation_time < ROTATION_COOLDOWN :
369395 return False
370396
371397 best : Optional [int ] = None
@@ -562,4 +588,6 @@ def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
562588
563589 # Unreachable: ``range(max_retries + 1)`` always yields at least one
564590 # iteration which either returns successfully or re-raises above.
565- raise AssertionError ("unreachable: retry loop exited without returning" )
591+ raise AssertionError ( # pragma: no cover
592+ "unreachable: retry loop exited without returning"
593+ )
0 commit comments