Repository: ethereum/web3.py
Pull request #3621 (2025-02-17 03:31:42)

Updated the copyright year to 2025 in .py file


Patch:
diff --git a/docs/conf.py b/docs/conf.py
index 9d42bc8c..63291829 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -52,7 +52,7 @@ master_doc = "index"
 
 # General information about the project.
 project = "web3.py"
-copyright = "2016-2024, The Ethereum Foundation"
+copyright = "2016-2025, The Ethereum Foundation"
 
 __version__ = setup_version
 # The version info for the project you're documenting, acts as replacement for
diff --git a/docs/providers.rst b/docs/providers.rst
index 97f318ed..9968189b 100644
--- a/docs/providers.rst
+++ b/docs/providers.rst
@@ -291,7 +291,7 @@ AsyncIPCProvider
 WebSocketProvider
 +++++++++++++++++
 
-.. py:class:: web3.providers.persistent.WebSocketProvider(endpoint_uri: str, websocket_kwargs: Dict[str, Any] = {})
+.. py:class:: web3.providers.persistent.WebSocketProvider(endpoint_uri: str, websocket_kwargs: Dict[str, Any] = {}, use_text_frames: bool = False)
 
     This provider handles interactions with an WS or WSS based JSON-RPC server.
 
@@ -299,6 +299,8 @@ WebSocketProvider
       ``'ws://localhost:8546'``.
     * ``websocket_kwargs`` this should be a dictionary of keyword arguments which
       will be passed onto the ws/wss websocket connection.
+    * ``use_text_frames`` will ensure websocket data is sent as text frames
+      for servers that do not support binary communication.
 
     This provider inherits from the
     :class:`~web3.providers.persistent.PersistentConnectionProvider` class. Refer to
diff --git a/newsfragments/3619.feature.rst b/newsfragments/3619.feature.rst
new file mode 100644
index 00000000..7561aeec
--- /dev/null
+++ b/newsfragments/3619.feature.rst
@@ -0,0 +1,1 @@
+Add `use_text_frames` flag for `WebSocketProvider` to work around websocket servers that don't support binary frames
diff --git a/web3/providers/persistent/websocket.py b/web3/providers/persistent/websocket.py
index 0a598049..0bd7dd79 100644
--- a/web3/providers/persistent/websocket.py
+++ b/web3/providers/persistent/websocket.py
@@ -63,6 +63,8 @@ class WebSocketProvider(PersistentConnectionProvider):
         self,
         endpoint_uri: Optional[Union[URI, str]] = None,
         websocket_kwargs: Optional[Dict[str, Any]] = None,
+        # uses binary frames by default
+        use_text_frames: Optional[bool] = False,
         # `PersistentConnectionProvider` kwargs can be passed through
         **kwargs: Any,
     ) -> None:
@@ -71,6 +73,7 @@ class WebSocketProvider(PersistentConnectionProvider):
             URI(endpoint_uri) if endpoint_uri is not None else get_default_endpoint()
         )
         super().__init__(**kwargs)
+        self.use_text_frames = use_text_frames
         self._ws: Optional[WebSocketClientProtocol] = None
 
         if not any(
@@ -118,9 +121,11 @@ class WebSocketProvider(PersistentConnectionProvider):
                 "Connection to websocket has not been initiated for the provider."
             )
 
-        await asyncio.wait_for(
-            self._ws.send(request_data), timeout=self.request_timeout
-        )
+        payload: Union[bytes, str] = request_data
+        if self.use_text_frames:
+            payload = request_data.decode("utf-8")
+
+        await asyncio.wait_for(self._ws.send(payload), timeout=self.request_timeout)
 
     async def socket_recv(self) -> RPCResponse:
         raw_response = await self._ws.recv()


Test patch:
diff --git a/tests/core/providers/test_websocket_provider.py b/tests/core/providers/test_websocket_provider.py
index 962920e4..bf18d10d 100644
--- a/tests/core/providers/test_websocket_provider.py
+++ b/tests/core/providers/test_websocket_provider.py
@@ -55,6 +55,14 @@ class WSException(Exception):
     pass
 
 
+GET_BLOCK_JSON_MESSAGE = {
+    "id": 0,
+    "jsonrpc": "2.0",
+    "method": "eth_getBlockByNumber",
+    "params": ["latest", False],
+}
+
+
 def test_get_endpoint_uri_or_ipc_path_returns_endpoint_uri():
     provider = WebSocketProvider("ws://mocked")
     assert (
@@ -67,6 +75,14 @@ def test_get_endpoint_uri_or_ipc_path_returns_endpoint_uri():
 # -- async -- #
 
 
+def test_websocket_provider_default_values():
+    ws_uri = "ws://127.0.0.1:1337"
+    with patch.dict("os.environ", {"WEB3_WS_PROVIDER_URI": ws_uri}):
+        provider = WebSocketProvider()
+        assert provider.endpoint_uri == ws_uri
+        assert provider.use_text_frames is False
+
+
 @pytest.mark.asyncio
 async def test_disconnect_cleanup():
     provider = WebSocketProvider("ws://mocked")
@@ -454,3 +470,26 @@ async def test_persistent_connection_provider_empty_batch_response():
             # assert that even though there was an error, we have reset the batching
             # state
             assert not async_w3.provider._is_batching
+
+
+@pytest.mark.parametrize(
+    "use_text_frames, expected_send_arg",
+    (
+        (False, to_bytes(text=json.dumps(GET_BLOCK_JSON_MESSAGE))),
+        (True, json.dumps(GET_BLOCK_JSON_MESSAGE)),
+    ),
+)
+@pytest.mark.asyncio
+async def test_websocket_provider_use_text_frames(use_text_frames, expected_send_arg):
+    provider = WebSocketProvider("ws://mocked", use_text_frames=use_text_frames)
+    assert provider.use_text_frames is use_text_frames
+
+    # mock provider and add a mocked response to the cache
+    _mock_ws(provider)
+    provider._ws.send = AsyncMock()
+    provider._request_processor._request_response_cache.cache(
+        generate_cache_key(0), "0x1337"
+    )
+
+    await provider.make_request(RPCEndpoint("eth_getBlockByNumber"), ["latest", False])
+    provider._ws.send.assert_called_once_with(expected_send_arg)