Repository: ethereum/web3.py

Issue:
async_http_retry_request_middleware
* Version: 6.4.0
* Python: 3.10
* OS: linux

how to use http_retry_request_middleware for AsyncWeb3? Is there a ready-made method? or should I write my own middleware?


Discussion:
We don't have an equivalent `http_retry_request_middleware` yet for AsyncWeb3 so you'll have to create your own. If you end up going that route and want to PR it, we'd be happy to review! I'll leave this open as a feature request. 

Resolving patch:
diff --git a/web3/middleware/__init__.py b/web3/middleware/__init__.py
--- a/web3/middleware/__init__.py
+++ b/web3/middleware/__init__.py
@@ -41,6 +41,7 @@
     construct_exception_handler_middleware,
 )
 from .exception_retry_request import (  # noqa: F401
+    async_http_retry_request_middleware,
     http_retry_request_middleware,
 )
 from .filter import (  # noqa: F401
diff --git a/web3/middleware/exception_retry_request.py b/web3/middleware/exception_retry_request.py
--- a/web3/middleware/exception_retry_request.py
+++ b/web3/middleware/exception_retry_request.py
@@ -1,11 +1,14 @@
+import asyncio
 from typing import (
     TYPE_CHECKING,
     Any,
     Callable,
     Collection,
+    Optional,
     Type,
 )
 
+import aiohttp
 from requests.exceptions import (
     ConnectionError,
     HTTPError,
@@ -14,12 +17,16 @@
 )
 
 from web3.types import (
+    AsyncMiddlewareCoroutine,
     RPCEndpoint,
     RPCResponse,
 )
 
 if TYPE_CHECKING:
-    from web3 import Web3  # noqa: F401
+    from web3 import (  # noqa: F401
+        AsyncWeb3,
+        Web3,
+    )
 
 whitelist = [
     "admin",
@@ -91,7 +98,7 @@ def check_if_retry_on_failure(method: RPCEndpoint) -> bool:
 
 def exception_retry_middleware(
     make_request: Callable[[RPCEndpoint, Any], RPCResponse],
-    w3: "Web3",
+    _w3: "Web3",
     errors: Collection[Type[BaseException]],
     retries: int = 5,
 ) -> Callable[[RPCEndpoint, Any], RPCResponse]:
@@ -100,13 +107,12 @@ def exception_retry_middleware(
     middleware for HTTPProvider.
     """
 
-    def middleware(method: RPCEndpoint, params: Any) -> RPCResponse:
+    def middleware(method: RPCEndpoint, params: Any) -> Optional[RPCResponse]:
         if check_if_retry_on_failure(method):
             for i in range(retries):
                 try:
                     return make_request(method, params)
-                # https://github.com/python/mypy/issues/5349
-                except errors:  # type: ignore
+                except tuple(errors):
                     if i < retries - 1:
                         continue
                     else:
@@ -124,3 +130,43 @@ def http_retry_request_middleware(
     return exception_retry_middleware(
         make_request, w3, (ConnectionError, HTTPError, Timeout, TooManyRedirects)
     )
+
+
+async def async_exception_retry_middleware(
+    make_request: Callable[[RPCEndpoint, Any], Any],
+    _async_w3: "AsyncWeb3",
+    errors: Collection[Type[BaseException]],
+    retries: int = 5,
+    backoff_factor: float = 0.3,
+) -> AsyncMiddlewareCoroutine:
+    """
+    Creates middleware that retries failed HTTP requests.
+    Is a default middleware for AsyncHTTPProvider.
+    """
+
+    async def middleware(method: RPCEndpoint, params: Any) -> Optional[RPCResponse]:
+        if check_if_retry_on_failure(method):
+            for i in range(retries):
+                try:
+                    return await make_request(method, params)
+                except tuple(errors):
+                    if i < retries - 1:
+                        await asyncio.sleep(backoff_factor)
+                        continue
+                    else:
+                        raise
+            return None
+        else:
+            return await make_request(method, params)
+
+    return middleware
+
+
+async def async_http_retry_request_middleware(
+    make_request: Callable[[RPCEndpoint, Any], Any], async_w3: "AsyncWeb3"
+) -> Callable[[RPCEndpoint, Any], Any]:
+    return await async_exception_retry_middleware(
+        make_request,
+        async_w3,
+        (TimeoutError, aiohttp.ClientError),
+    )
diff --git a/web3/providers/async_rpc.py b/web3/providers/async_rpc.py
--- a/web3/providers/async_rpc.py
+++ b/web3/providers/async_rpc.py
@@ -27,10 +27,17 @@
     get_default_http_endpoint,
 )
 from web3.types import (
+    AsyncMiddleware,
     RPCEndpoint,
     RPCResponse,
 )
 
+from ..datastructures import (
+    NamedElementOnion,
+)
+from ..middleware.exception_retry_request import (
+    async_http_retry_request_middleware,
+)
 from .async_base import (
     AsyncJSONBaseProvider,
 )
@@ -40,6 +47,8 @@ class AsyncHTTPProvider(AsyncJSONBaseProvider):
     logger = logging.getLogger("web3.providers.HTTPProvider")
     endpoint_uri = None
     _request_kwargs = None
+    # type ignored b/c conflict with _middlewares attr on AsyncBaseProvider
+    _middlewares: Tuple[AsyncMiddleware, ...] = NamedElementOnion([(async_http_retry_request_middleware, "http_retry_request")])  # type: ignore # noqa: E501
 
     def __init__(
         self,


Test patch:
diff --git a/tests/core/middleware/test_http_request_retry.py b/tests/core/middleware/test_http_request_retry.py
--- a/tests/core/middleware/test_http_request_retry.py
+++ b/tests/core/middleware/test_http_request_retry.py
@@ -4,6 +4,8 @@
     patch,
 )
 
+import aiohttp
+import pytest_asyncio
 from requests.exceptions import (
     ConnectionError,
     HTTPError,
@@ -12,7 +14,12 @@
 )
 
 import web3
+from web3 import (
+    AsyncHTTPProvider,
+    AsyncWeb3,
+)
 from web3.middleware.exception_retry_request import (
+    async_exception_retry_middleware,
     check_if_retry_on_failure,
     exception_retry_middleware,
 )
@@ -90,3 +97,62 @@ def test_check_with_all_middlewares(make_post_request_mock):
     with pytest.raises(ConnectionError):
         w3.eth.block_number
     assert make_post_request_mock.call_count == 5
+
+
+# -- async -- #
+
+
+ASYNC_TEST_RETRY_COUNT = 3
+
+
+@pytest_asyncio.fixture
+async def async_exception_retry_request_setup():
+    w3 = Mock()
+    provider = AsyncHTTPProvider()
+    setup = await async_exception_retry_middleware(
+        provider.make_request,
+        w3,
+        (TimeoutError, aiohttp.ClientError),
+        ASYNC_TEST_RETRY_COUNT,
+        0.1,
+    )
+    setup.w3 = w3
+    return setup
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+    "error",
+    (
+        TimeoutError,
+        aiohttp.ClientError,  # general base class for all aiohttp errors
+        # ClientError subclasses
+        aiohttp.ClientConnectionError,
+        aiohttp.ServerTimeoutError,
+        aiohttp.ClientOSError,
+    ),
+)
+async def test_check_retry_middleware(error, async_exception_retry_request_setup):
+    with patch(
+        "web3.providers.async_rpc.async_make_post_request"
+    ) as make_post_request_mock:
+        make_post_request_mock.side_effect = error
+
+        with pytest.raises(error):
+            await async_exception_retry_request_setup("eth_getBalance", [])
+        assert make_post_request_mock.call_count == ASYNC_TEST_RETRY_COUNT
+
+
+@pytest.mark.asyncio
+async def test_check_without_retry_middleware():
+    with patch(
+        "web3.providers.async_rpc.async_make_post_request"
+    ) as make_post_request_mock:
+        make_post_request_mock.side_effect = TimeoutError
+        provider = AsyncHTTPProvider()
+        w3 = AsyncWeb3(provider)
+        w3.provider._middlewares = ()
+
+        with pytest.raises(TimeoutError):
+            await w3.eth.block_number
+        assert make_post_request_mock.call_count == 1