Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add flag to is_connected to allow user to see why provider connection failed #2912

Merged
merged 9 commits into from
Apr 24, 2023
Merged
6 changes: 5 additions & 1 deletion docs/internals.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,17 @@ setting the middlewares the provider should use.
the JSON-RPC method being called.


.. py:method:: BaseProvider.is_connected()
.. py:method:: BaseProvider.is_connected(show_traceback=False)

This function should return ``True`` or ``False`` depending on whether the
provider should be considered *connected*. For example, an IPC socket
based provider should return ``True`` if the socket is open and ``False``
if the socket is closed.

If set to ``True``, the optional ``show_traceback`` boolean will raise a
``ProviderConnectionError`` and provide information on why the provider should
not be considered *connected*.


.. py:attribute:: BaseProvider.middlewares

Expand Down
16 changes: 13 additions & 3 deletions docs/troubleshooting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,19 @@ You can check that your instance is connected via the ``is_connected`` method:
>>> w3.is_connected()
False

There's a variety of explanations for why you may see ``False`` here. If you're
running a local node, such as Geth, double-check that you've indeed started the
binary and that you've started it from the intended directory - particularly if
There are a variety of explanations for why you may see ``False`` here. To help you
diagnose the problem, ``is_connected`` has an optional ``show_traceback`` argument:

.. code-block:: python

>>> w3.is_connected(show_traceback=True)
# this is an example, your error may differ

# <long stack trace ouput>
ProviderConnectionError: Problem connecting to provider with error: <class 'FileNotFoundError'>: cannot connect to IPC socket at path: None

If you're running a local node, such as Geth, double-check that you've indeed started
the binary and that you've started it from the intended directory - particularly if
you've specified a relative path to its ipc file.

If that does not address your issue, it's probable that you still have a
Expand Down
1 change: 1 addition & 0 deletions newsfragments/2912.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add show_traceback flag to is_connected to allow user to see connection error reason
19 changes: 18 additions & 1 deletion tests/core/providers/test_async_http_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
from web3.eth import (
AsyncEth,
)
from web3.exceptions import (
ProviderConnectionError,
)
from web3.geth import (
AsyncGeth,
AsyncGethAdmin,
Expand All @@ -35,11 +38,25 @@
URI = "http://mynode.local:8545"


def test_no_args():
async def clean_async_session_cache():
cache_data = request._async_session_cache._data
while len(cache_data) > 0:
_key, cached_session = cache_data.popitem()
await cached_session.close()


@pytest.mark.asyncio
async def test_no_args() -> None:
provider = AsyncHTTPProvider()
w3 = AsyncWeb3(provider)
assert w3.manager.provider == provider
assert w3.manager.provider.is_async
assert not await w3.is_connected()
with pytest.raises(ProviderConnectionError):
await w3.is_connected(show_traceback=True)

await clean_async_session_cache()
assert len(request._async_session_cache) == 0


def test_init_kwargs():
Expand Down
57 changes: 57 additions & 0 deletions tests/core/providers/test_base_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from web3 import (
Web3,
)
from web3.providers import (
AutoProvider,
BaseProvider,
)


class ConnectedProvider(BaseProvider):
def is_connected(self, show_traceback: bool = False):
return True


class DisconnectedProvider(BaseProvider):
def is_connected(self, show_traceback: bool = False):
return False


def test_is_connected_connected():
"""
Web3.is_connected() returns True when connected to a node.
"""
w3 = Web3(ConnectedProvider())
assert w3.is_connected() is True


def test_is_connected_disconnected():
"""
Web3.is_connected() returns False when configured with a provider
that's not connected to a node.
"""
w3 = Web3(DisconnectedProvider())
assert w3.is_connected() is False


def test_autoprovider_detection():
def no_provider():
return None

def must_not_call():
assert False

auto = AutoProvider(
[
no_provider,
DisconnectedProvider,
ConnectedProvider,
must_not_call,
]
)

w3 = Web3(auto)

assert w3.is_connected()

assert isinstance(auto._active_provider, ConnectedProvider)
8 changes: 8 additions & 0 deletions tests/core/providers/test_http_provider.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from requests import (
Session,
)
Expand All @@ -11,6 +13,9 @@
from web3._utils import (
request,
)
from web3.exceptions import (
ProviderConnectionError,
)
from web3.providers import (
HTTPProvider,
)
Expand All @@ -23,6 +28,9 @@ def test_no_args():
w3 = Web3(provider)
assert w3.manager.provider == provider
assert not w3.manager.provider.is_async
assert not w3.is_connected()
with pytest.raises(ProviderConnectionError):
w3.is_connected(show_traceback=True)


def test_init_kwargs():
Expand Down
5 changes: 5 additions & 0 deletions tests/core/providers/test_ipc_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
from web3.auto.gethdev import (
w3,
)
from web3.exceptions import (
ProviderConnectionError,
)
from web3.middleware import (
construct_fixture_middleware,
)
Expand All @@ -37,6 +40,8 @@ def test_ipc_no_path():
"""
ipc = IPCProvider(None)
assert ipc.is_connected() is False
with pytest.raises(ProviderConnectionError):
ipc.is_connected(show_traceback=True)


def test_ipc_tilda_in_path():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,65 +6,30 @@

from web3 import (
EthereumTesterProvider,
Web3,
)
from web3.providers import (
AutoProvider,
BaseProvider,
)
from web3.types import (
RPCEndpoint,
)


class ConnectedProvider(BaseProvider):
def is_connected(self):
return True


class DisconnectedProvider(BaseProvider):
def is_connected(self):
return False


def test_is_connected_connected():
"""
Web3.is_connected() returns True when connected to a node.
"""
w3 = Web3(ConnectedProvider())
assert w3.is_connected() is True


def test_is_connected_disconnected():
"""
Web3.is_connected() returns False when configured with a provider
that's not connected to a node.
"""
w3 = Web3(DisconnectedProvider())
assert w3.is_connected() is False


def test_autoprovider_detection():
def no_provider():
return None
def test_tester_provider_is_connected() -> None:
provider = EthereumTesterProvider()
connected = provider.is_connected()
assert connected

def must_not_call():
assert False

auto = AutoProvider(
[
no_provider,
DisconnectedProvider,
ConnectedProvider,
must_not_call,
]
def test_tester_provider_creates_a_block() -> None:
provider = EthereumTesterProvider()
accounts = provider.make_request("eth_accounts", [])
a, b = accounts["result"][:2]
current_block = provider.make_request("eth_blockNumber", [])
assert current_block["result"] == 0
tx = provider.make_request(
"eth_sendTransaction", [{"from": a, "to": b, "gas": 21000}]
)

w3 = Web3(auto)

assert w3.is_connected()

assert isinstance(auto._active_provider, ConnectedProvider)
assert tx
current_block = provider.make_request("eth_blockNumber", [])
assert current_block["result"] == 1


@pytest.mark.parametrize(
Expand Down
11 changes: 11 additions & 0 deletions tests/core/providers/test_websocket_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
Web3,
)
from web3.exceptions import (
ProviderConnectionError,
Web3ValidationError,
)
from web3.providers.websocket import (
Expand Down Expand Up @@ -63,6 +64,16 @@ def w3(open_port, start_websocket_server):
return Web3(provider)


def test_no_args():
provider = WebsocketProvider()
w3 = Web3(provider)
assert w3.manager.provider == provider
assert not w3.manager.provider.is_async
assert not w3.is_connected()
with pytest.raises(ProviderConnectionError):
w3.is_connected(show_traceback=True)


def test_websocket_provider_timeout(w3):
with pytest.raises(TimeoutError):
w3.eth.accounts
Expand Down
8 changes: 8 additions & 0 deletions web3/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ class BlockNumberOutofRange(Web3Exception):
pass


class ProviderConnectionError(Web3Exception):
"""
Raised when unable to connect to a provider
"""

pass


class CannotHandleRequest(Web3Exception):
"""
Raised by a provider to signal that it cannot handle an RPC request and
Expand Down
8 changes: 4 additions & 4 deletions web3/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,8 @@ def __init__(

self.ens = ens

def is_connected(self) -> Coroutine[Any, Any, bool]:
return self.provider.is_connected()
def is_connected(self, show_traceback: bool = False) -> Coroutine[Any, Any, bool]:
return self.provider.is_connected(show_traceback)

@property
def middleware_onion(self) -> AsyncMiddlewareOnion:
Expand Down Expand Up @@ -441,8 +441,8 @@ def __init__(

self.ens = ens

def is_connected(self) -> bool:
return self.provider.is_connected()
def is_connected(self, show_traceback: bool = False) -> bool:
return self.provider.is_connected(show_traceback)

@classmethod
def normalize_values(
Expand Down
28 changes: 22 additions & 6 deletions web3/providers/async_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
from web3._utils.encoding import (
FriendlyJsonSerde,
)
from web3.exceptions import (
ProviderConnectionError,
)
from web3.middleware import (
async_combine_middlewares,
)
Expand Down Expand Up @@ -81,7 +84,7 @@ async def _generate_request_func(
async def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
raise NotImplementedError("Providers must implement this method")

async def is_connected(self) -> bool:
async def is_connected(self, show_traceback: bool = False) -> bool:
raise NotImplementedError("Providers must implement this method")


Expand All @@ -104,13 +107,26 @@ def decode_rpc_response(self, raw_response: bytes) -> RPCResponse:
text_response = to_text(raw_response)
return cast(RPCResponse, FriendlyJsonSerde().json_decode(text_response))

async def is_connected(self) -> bool:
async def is_connected(self, show_traceback: bool = False) -> bool:
try:
response = await self.make_request(RPCEndpoint("web3_clientVersion"), [])
except OSError:
except OSError as e:
if show_traceback:
raise ProviderConnectionError(
f"Problem connecting to provider with error: {type(e)}: {e}"
)
return False

assert response["jsonrpc"] == "2.0"
assert "error" not in response
if "error" in response:
if show_traceback:
raise ProviderConnectionError(
f"Error received from provider: {response}"
)
return False

return True
if response["jsonrpc"] == "2.0":
return True
else:
if show_traceback:
raise ProviderConnectionError(f"Bad jsonrpc version: {response}")
return False
4 changes: 2 additions & 2 deletions web3/providers/auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
except OSError:
return self._proxy_request(method, params, use_cache=False)

def is_connected(self) -> bool:
def is_connected(self, show_traceback: bool = False) -> bool:
provider = self._get_active_provider(use_cache=True)
return provider is not None and provider.is_connected()
return provider is not None and provider.is_connected(show_traceback)

def _proxy_request(
self, method: RPCEndpoint, params: Any, use_cache: bool = True
Expand Down
Loading