Repository: ethereum/web3.py

Issue:
Connection to ropsten websocket over infura hang.
* Version: 4.4.1
* Python: 3.6
* OS: osx


### What was wrong?

I wasn't able to connect to infura's ropsten websocket. The process hung when I wanted to create the filter object on pending transactions. It worked for both rinkeby's and mainnet's but just not for ropsten network.

I was wondering if anyone is having the same issue. Given that I was able to connect to two other networks it makes it less suspicious that the problem is caused by the web3.py

Below is how I instantiate the filter by following https://web3py.readthedocs.io/en/stable/filters.html#web3.utils.filters.TransactionFilter
```python
In [23]: web3 = Web3(Web3.WebsocketProvider('wss://rinkeby.infura.io/_ws'))

In [24]: new_transaction_filter = web3.eth.filter('pending')

In [25]: new_transaction_filter.get_new_entries()
Out[25]:
[HexBytes('0x4945f39cbeedd911eae93577c75d763d364c37ae01dcb7fb31ff9421a2a92b71'),
 HexBytes('0xae0b88b85163d3f0a918236cbf3ec93ccb589c6ad327e937f69b9591680fbf61'),
```

Process hung on line 27 for a long time. I interrupted. The stacktrace is below.
```python
In [26]: web3 = Web3(Web3.WebsocketProvider('wss://ropsten.infura.io/ws'))

In [27]: new_transaction_filter = web3.eth.filter('pending')

^C---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-3-60afc0d44840> in <module>()
----> 1 new_transaction_filter = web3.eth.filter('pending')

~/pg/some_name/virtualenv_run/lib/python3.6/site-packages/web3/eth.py in filter(self, filter_params, filter_id)
    319             elif filter_params == "pending":
    320                 filter_id = self.web3.manager.request_blocking(
--> 321                     "eth_newPendingTransactionFilter", [],
    322                 )
    323                 return TransactionFilter(self.web3, filter_id)

~/pg/some_name/virtualenv_run/lib/python3.6/site-packages/web3/manager.py in request_blocking(self, method, params)
    105         Make a synchronous request using the provider
    106         """
--> 107         response = self._make_request(method, params)
    108
    109         if "error" in response:

~/pg/some_name/virtualenv_run/lib/python3.6/site-packages/web3/manager.py in _make_request(self, method, params)
     88             self.logger.debug("Making request. Method: %s", method)
     89             try:
---> 90                 return request_func(method, params)
     91             except CannotHandleRequest:
     92                 continue

~/pg/some_name/virtualenv_run/lib/python3.6/site-packages/web3/middleware/gas_price_strategy.py in middleware(method, params)
     16                     transaction = assoc(transaction, 'gasPrice', generated_gas_price)
     17                     return make_request(method, [transaction])
---> 18         return make_request(method, params)
     19     return middleware

~/pg/some_name/virtualenv_run/lib/python3.6/site-packages/cytoolz/functoolz.pyx in cytoolz.functoolz.curry.__call__()

~/pg/some_name/virtualenv_run/lib/python3.6/site-packages/web3/middleware/formatting.py in apply_formatters(method, params, make_request, request_formatters, result_formatters, error_formatters)
     48         response = make_request(method, formatted_params)
     49     else:
---> 50         response = make_request(method, params)
     51
     52     if 'result' in response and method in result_formatters:

~/pg/some_name/virtualenv_run/lib/python3.6/site-packages/web3/middleware/attrdict.py in middleware(method, params)
     16     """
     17     def middleware(method, params):
---> 18         response = make_request(method, params)
     19
     20         if 'result' in response:

~/pg/some_name/virtualenv_run/lib/python3.6/site-packages/cytoolz/functoolz.pyx in cytoolz.functoolz.curry.__call__()

~/pg/some_name/virtualenv_run/lib/python3.6/site-packages/web3/middleware/formatting.py in apply_formatters(method, params, make_request, request_formatters, result_formatters, error_formatters)
     48         response = make_request(method, formatted_params)
     49     else:
---> 50         response = make_request(method, params)
     51
     52     if 'result' in response and method in result_formatters:

~/pg/some_name/virtualenv_run/lib/python3.6/site-packages/web3/middleware/normalize_errors.py in middleware(method, params)
      7 def normalize_errors_middleware(make_request, web3):
      8     def middleware(method, params):
----> 9         result = make_request(method, params)
     10
     11         # As of v1.8, Geth returns errors when you request a

~/pg/some_name/virtualenv_run/lib/python3.6/site-packages/cytoolz/functoolz.pyx in cytoolz.functoolz.curry.__call__()

~/pg/some_name/virtualenv_run/lib/python3.6/site-packages/web3/middleware/formatting.py in apply_formatters(method, params, make_request, request_formatters, result_formatters, error_formatters)
     48         response = make_request(method, formatted_params)
     49     else:
---> 50         response = make_request(method, params)
     51
     52     if 'result' in response and method in result_formatters:

~/pg/some_name/virtualenv_run/lib/python3.6/site-packages/cytoolz/functoolz.pyx in cytoolz.functoolz.curry.__call__()

~/pg/some_name/virtualenv_run/lib/python3.6/site-packages/web3/middleware/formatting.py in apply_formatters(method, params, make_request, request_formatters, result_formatters, error_formatters)
     48         response = make_request(method, formatted_params)
     49     else:
---> 50         response = make_request(method, params)
     51
     52     if 'result' in response and method in result_formatters:

~/pg/some_name/virtualenv_run/lib/python3.6/site-packages/web3/providers/websocket.py in make_request(self, method, params)
    102             WebsocketProvider._loop
    103         )
--> 104         return future.result()

/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/concurrent/futures/_base.py in result(self, timeout)
    425                 return self.__get_result()
    426
--> 427             self._condition.wait(timeout)
    428
    429             if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:

/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/threading.py in wait(self, timeout)
    293         try:    # restore state no matter what (e.g., KeyboardInterrupt)
    294             if timeout is None:
--> 295                 waiter.acquire()
    296                 gotit = True
    297             else:

KeyboardInterrupt:
```

I have tried both `wss://ropsten.infura.io/ws` and `wss://ropsten.infura.io/_ws`. I have also posted the same question on Infura forum (https://github.com/INFURA/infura/issues/110). Trying to get help on both sides.

### How can it be fixed?

TBD.



Discussion:
AFAIK, Infura doesn't support filters.

I'll leave this open, though, as a reminder to produce a better error message. We shouldn't hang indefinitely on an Infura failure.

cc @voith any ideas for improvements there?
@carver: Thanks for the reply here. Interesting, both rinkeby and mainnet filters work for me through infura. If I really need to get the pending transaction filter, is hosting my own local node the only way to go? Or is there any other providers like Infura that I could go for?
>  If I really need to get the pending transaction filter, is hosting my own local node the only way to go? Or is there any other providers like Infura that I could go for?

Running your local node is the only solution I'm familiar with for getting pending transactions.
@FYJen Thanks for the report!
I think we probably need to add some default timeout here. IIRC, `websockets` waits infinitely if `timeout` is None.
I'll take care of this. But maybe in another 2-3 days(little busy these days).
Turns out that the `timeout` parameter of `websockets` does not work as expected!
citing from the documentation:
> The timeout parameter defines the maximum wait time in seconds for completing the closing handshake and, only on the client side, for terminating the TCP connection. close() will complete in at most 4 * timeout on the server side and 5 * timeout on the client side.

`websockets` does not provide any other mechanism to timeout connections.

The author suggests using `with asyncio.wait_for(connect(...), your_timeout)`. You can read it [here](https://github.com/aaugustin/websockets/issues/428#issuecomment-397858402).

@carver any thoughts? Can we use the `CancelToken` library here? I haven't dug in but I'm assuming that  canceltoken is some general purpose solution to be used for such cases.
A cancel token is more useful when there are several services running that all need to cancel simultaneously. The wait_for solution seems fine.
`asyncio.wait_for` is probably the better solution for now if there's only a small number of call sites that need timeouts.  The `CancelToken` pattern is more about cascading cancellations for a complex application so that you can trigger cancellation in one spot and all child *things* also get canceled.
@carver @pipermerriam Thank you for the explanation!
This is a bug in web3 and has been noted in https://github.com/ethereum/web3.py/issues/1009#issuecomment-414537934. The connection timeout error needs to be fixed too.
@carver: From the [documentation](https://infura.io/docs/wss/eth_newPendingTransactionFilter), Infura's WebSocket API officially supports `eth_newPendingTransactionFilter` JSON RPC.
@FYJen AFAIK, filters weren't supported by infura for some time. They must have been added recently.
Anyway, this is a bug and should be fixed. 
I'm sorry, I don't get enough free time to code these days. I haven't forgotten about the issue and I'll return to it as soon as I can.

Resolving patch:
diff --git a/web3/providers/websocket.py b/web3/providers/websocket.py
--- a/web3/providers/websocket.py
+++ b/web3/providers/websocket.py
@@ -16,6 +16,7 @@
 )
 
 RESTRICTED_WEBSOCKET_KWARGS = {'uri', 'loop'}
+DEFAULT_WEBSOCKET_TIMEOUT = 10
 
 
 def _start_event_loop(loop):
@@ -63,8 +64,14 @@ class WebsocketProvider(JSONBaseProvider):
     logger = logging.getLogger("web3.providers.WebsocketProvider")
     _loop = None
 
-    def __init__(self, endpoint_uri=None, websocket_kwargs=None):
+    def __init__(
+            self,
+            endpoint_uri=None,
+            websocket_kwargs=None,
+            websocket_timeout=DEFAULT_WEBSOCKET_TIMEOUT
+    ):
         self.endpoint_uri = endpoint_uri
+        self.websocket_timeout = websocket_timeout
         if self.endpoint_uri is None:
             self.endpoint_uri = get_default_endpoint()
         if WebsocketProvider._loop is None:
@@ -90,8 +97,16 @@ def __str__(self):
 
     async def coro_make_request(self, request_data):
         async with self.conn as conn:
-            await conn.send(request_data)
-            return json.loads(await conn.recv())
+            await asyncio.wait_for(
+                conn.send(request_data),
+                timeout=self.websocket_timeout
+            )
+            return json.loads(
+                await asyncio.wait_for(
+                    conn.recv(),
+                    timeout=self.websocket_timeout
+                )
+            )
 
     def make_request(self, method, params):
         self.logger.debug("Making request WebSocket. URI: %s, "


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -8,7 +8,16 @@
     identity,
 )
 
+from .utils import (
+    get_open_port,
+)
+
 
 @pytest.fixture(scope="module", params=[lambda x: to_bytes(hexstr=x), identity])
 def address_conversion_func(request):
     return request.param
+
+
+@pytest.fixture()
+def open_port():
+    return get_open_port()
diff --git a/tests/core/providers/test_websocket_provider.py b/tests/core/providers/test_websocket_provider.py
--- a/tests/core/providers/test_websocket_provider.py
+++ b/tests/core/providers/test_websocket_provider.py
@@ -1,5 +1,18 @@
+import asyncio
+from concurrent.futures import (
+    TimeoutError,
+)
 import pytest
+from threading import (
+    Thread,
+)
+
+import websockets
 
+from tests.utils import (
+    wait_for_ws,
+)
+from web3 import Web3
 from web3.exceptions import (
     ValidationError,
 )
@@ -8,6 +21,42 @@
 )
 
 
+@pytest.yield_fixture
+def start_websocket_server(open_port):
+    event_loop = asyncio.new_event_loop()
+
+    def run_server():
+        async def empty_server(websocket, path):
+            data = await websocket.recv()
+            await asyncio.sleep(0.02)
+            await websocket.send(data)
+        server = websockets.serve(empty_server, '127.0.0.1', open_port, loop=event_loop)
+        event_loop.run_until_complete(server)
+        event_loop.run_forever()
+
+    thd = Thread(target=run_server)
+    thd.start()
+    try:
+        yield
+    finally:
+        event_loop.call_soon_threadsafe(event_loop.stop)
+
+
+@pytest.fixture()
+def w3(open_port, start_websocket_server):
+    # need new event loop as the one used by server is already running
+    event_loop = asyncio.new_event_loop()
+    endpoint_uri = 'ws://127.0.0.1:{}'.format(open_port)
+    event_loop.run_until_complete(wait_for_ws(endpoint_uri, event_loop))
+    provider = WebsocketProvider(endpoint_uri, websocket_timeout=0.01)
+    return Web3(provider)
+
+
+def test_websocket_provider_timeout(w3):
+    with pytest.raises(TimeoutError):
+        w3.eth.accounts
+
+
 def test_restricted_websocket_kwargs():
     invalid_kwargs = {'uri': 'ws://127.0.0.1:8546'}
     re_exc_message = r'.*found: {0}*'.format(set(invalid_kwargs.keys()))
diff --git a/tests/generate_go_ethereum_fixture.py b/tests/generate_go_ethereum_fixture.py
--- a/tests/generate_go_ethereum_fixture.py
+++ b/tests/generate_go_ethereum_fixture.py
@@ -22,6 +22,9 @@
     to_wei,
 )
 
+from tests.utils import (
+    get_open_port,
+)
 from web3 import Web3
 from web3._utils.module_testing.emitter_contract import (
     EMITTER_ABI,
@@ -100,14 +103,6 @@ def tempdir():
         shutil.rmtree(dir_path)
 
 
-def get_open_port():
-    sock = socket.socket()
-    sock.bind(('127.0.0.1', 0))
-    port = sock.getsockname()[1]
-    sock.close()
-    return str(port)
-
-
 def get_geth_binary():
     from geth.install import (
         get_executable_path,
diff --git a/tests/integration/generate_fixtures/common.py b/tests/integration/generate_fixtures/common.py
--- a/tests/integration/generate_fixtures/common.py
+++ b/tests/integration/generate_fixtures/common.py
@@ -76,14 +76,6 @@ def tempdir():
         shutil.rmtree(dir_path)
 
 
-def get_open_port():
-    sock = socket.socket()
-    sock.bind(('127.0.0.1', 0))
-    port = sock.getsockname()[1]
-    sock.close()
-    return str(port)
-
-
 def get_geth_binary():
     from geth.install import (
         get_executable_path,
diff --git a/tests/integration/generate_fixtures/go_ethereum.py b/tests/integration/generate_fixtures/go_ethereum.py
--- a/tests/integration/generate_fixtures/go_ethereum.py
+++ b/tests/integration/generate_fixtures/go_ethereum.py
@@ -11,6 +11,9 @@
 )
 
 import common
+from tests.utils import (
+    get_open_port,
+)
 from web3 import Web3
 from web3._utils.module_testing.emitter_contract import (
     EMITTER_ABI,
@@ -42,7 +45,7 @@ def generate_go_ethereum_fixture(destination_dir):
         geth_ipc_path_dir = stack.enter_context(common.tempdir())
         geth_ipc_path = os.path.join(geth_ipc_path_dir, 'geth.ipc')
 
-        geth_port = common.get_open_port()
+        geth_port = get_open_port()
         geth_binary = common.get_geth_binary()
 
         geth_proc = stack.enter_context(common.get_geth_process(  # noqa: F841
diff --git a/tests/integration/generate_fixtures/parity.py b/tests/integration/generate_fixtures/parity.py
--- a/tests/integration/generate_fixtures/parity.py
+++ b/tests/integration/generate_fixtures/parity.py
@@ -12,6 +12,9 @@
 
 import common
 import go_ethereum
+from tests.utils import (
+    get_open_port,
+)
 from web3 import Web3
 from web3._utils.toolz import (
     merge,
@@ -176,7 +179,7 @@ def generate_parity_fixture(destination_dir):
 
         geth_datadir = stack.enter_context(common.tempdir())
 
-        geth_port = common.get_open_port()
+        geth_port = get_open_port()
 
         geth_ipc_path_dir = stack.enter_context(common.tempdir())
         geth_ipc_path = os.path.join(geth_ipc_path_dir, 'geth.ipc')
@@ -221,7 +224,7 @@ def generate_parity_fixture(destination_dir):
         parity_ipc_path_dir = stack.enter_context(common.tempdir())
         parity_ipc_path = os.path.join(parity_ipc_path_dir, 'jsonrpc.ipc')
 
-        parity_port = common.get_open_port()
+        parity_port = get_open_port()
         parity_binary = get_parity_binary()
 
         parity_proc = stack.enter_context(get_parity_process(  # noqa: F841
diff --git a/tests/integration/go_ethereum/common.py b/tests/integration/go_ethereum/common.py
--- a/tests/integration/go_ethereum/common.py
+++ b/tests/integration/go_ethereum/common.py
@@ -1,5 +1,4 @@
 import pytest
-import socket
 
 from web3._utils.module_testing import (
     EthModuleTest,
@@ -10,14 +9,6 @@
 )
 
 
-def get_open_port():
-    sock = socket.socket()
-    sock.bind(('127.0.0.1', 0))
-    port = sock.getsockname()[1]
-    sock.close()
-    return str(port)
-
-
 class GoEthereumTest(Web3ModuleTest):
     def _check_web3_clientVersion(self, client_version):
         assert client_version.startswith('Geth/')
diff --git a/tests/integration/go_ethereum/test_goethereum_http.py b/tests/integration/go_ethereum/test_goethereum_http.py
--- a/tests/integration/go_ethereum/test_goethereum_http.py
+++ b/tests/integration/go_ethereum/test_goethereum_http.py
@@ -1,5 +1,8 @@
 import pytest
 
+from tests.utils import (
+    get_open_port,
+)
 from web3 import Web3
 
 from .common import (
@@ -8,7 +11,6 @@
     GoEthereumPersonalModuleTest,
     GoEthereumTest,
     GoEthereumVersionModuleTest,
-    get_open_port,
 )
 from .utils import (
     wait_for_http,
diff --git a/tests/integration/go_ethereum/test_goethereum_ipc.py b/tests/integration/go_ethereum/test_goethereum_ipc.py
--- a/tests/integration/go_ethereum/test_goethereum_ipc.py
+++ b/tests/integration/go_ethereum/test_goethereum_ipc.py
@@ -2,6 +2,9 @@
 import pytest
 import tempfile
 
+from tests.utils import (
+    get_open_port,
+)
 from web3 import Web3
 
 from .common import (
@@ -12,7 +15,6 @@
     GoEthereumVersionModuleTest,
 )
 from .utils import (
-    get_open_port,
     wait_for_socket,
 )
 
diff --git a/tests/integration/go_ethereum/test_goethereum_ws.py b/tests/integration/go_ethereum/test_goethereum_ws.py
--- a/tests/integration/go_ethereum/test_goethereum_ws.py
+++ b/tests/integration/go_ethereum/test_goethereum_ws.py
@@ -3,7 +3,8 @@
 from tests.integration.common import (
     MiscWebsocketTest,
 )
-from tests.integration.utils import (
+from tests.utils import (
+    get_open_port,
     wait_for_ws,
 )
 from web3 import Web3
@@ -14,7 +15,6 @@
     GoEthereumPersonalModuleTest,
     GoEthereumTest,
     GoEthereumVersionModuleTest,
-    get_open_port,
 )
 
 
diff --git a/tests/integration/go_ethereum/utils.py b/tests/integration/go_ethereum/utils.py
--- a/tests/integration/go_ethereum/utils.py
+++ b/tests/integration/go_ethereum/utils.py
@@ -50,11 +50,3 @@ def kill_proc_gracefully(proc):
     if proc.poll() is None:
         proc.kill()
         wait_for_popen(proc, 2)
-
-
-def get_open_port():
-    sock = socket.socket()
-    sock.bind(('127.0.0.1', 0))
-    port = sock.getsockname()[1]
-    sock.close()
-    return str(port)
diff --git a/tests/integration/parity/common.py b/tests/integration/parity/common.py
--- a/tests/integration/parity/common.py
+++ b/tests/integration/parity/common.py
@@ -1,5 +1,4 @@
 import pytest
-import socket
 
 from flaky import (
     flaky,
@@ -16,14 +15,6 @@
 MAX_FLAKY_RUNS = 3
 
 
-def get_open_port():
-    sock = socket.socket()
-    sock.bind(('127.0.0.1', 0))
-    port = sock.getsockname()[1]
-    sock.close()
-    return str(port)
-
-
 class ParityWeb3ModuleTest(Web3ModuleTest):
     def _check_web3_clientVersion(self, client_version):
         assert client_version.startswith('Parity/')
diff --git a/tests/integration/parity/test_parity_http.py b/tests/integration/parity/test_parity_http.py
--- a/tests/integration/parity/test_parity_http.py
+++ b/tests/integration/parity/test_parity_http.py
@@ -4,6 +4,9 @@
 from tests.integration.parity.utils import (
     wait_for_http,
 )
+from tests.utils import (
+    get_open_port,
+)
 from web3 import Web3
 from web3._utils.module_testing import (
     NetModuleTest,
@@ -15,7 +18,6 @@
     ParityPersonalModuleTest,
     ParityTraceModuleTest,
     ParityWeb3ModuleTest,
-    get_open_port,
 )
 
 
diff --git a/tests/integration/parity/test_parity_ws.py b/tests/integration/parity/test_parity_ws.py
--- a/tests/integration/parity/test_parity_ws.py
+++ b/tests/integration/parity/test_parity_ws.py
@@ -4,7 +4,8 @@
 from tests.integration.common import (
     MiscWebsocketTest,
 )
-from tests.integration.utils import (
+from tests.utils import (
+    get_open_port,
     wait_for_ws,
 )
 from web3 import Web3
@@ -18,7 +19,6 @@
     ParityPersonalModuleTest,
     ParityTraceModuleTest,
     ParityWeb3ModuleTest,
-    get_open_port,
 )
 
 
diff --git a/tests/integration/parity/utils.py b/tests/integration/parity/utils.py
--- a/tests/integration/parity/utils.py
+++ b/tests/integration/parity/utils.py
@@ -80,11 +80,3 @@ def kill_proc_gracefully(proc):
     if proc.poll() is None:
         proc.kill()
         wait_for_popen(proc, 2)
-
-
-def get_open_port():
-    sock = socket.socket()
-    sock.bind(('127.0.0.1', 0))
-    port = sock.getsockname()[1]
-    sock.close()
-    return str(port)
diff --git a/tests/integration/utils.py b/tests/utils.py
similarity index 70%
rename from tests/integration/utils.py
rename to tests/utils.py
--- a/tests/integration/utils.py
+++ b/tests/utils.py
@@ -1,9 +1,18 @@
 import asyncio
+import socket
 import time
 
 import websockets
 
 
+def get_open_port():
+    sock = socket.socket()
+    sock.bind(('127.0.0.1', 0))
+    port = sock.getsockname()[1]
+    sock.close()
+    return str(port)
+
+
 async def wait_for_ws(endpoint_uri, event_loop, timeout=60):
     start = time.time()
     while time.time() < start + timeout: