Skip to content

Commit 05cee68

Browse files
committed
Explicitly raise RequestTimedOut on timed out requests:
- Introduce ``RequestTimedOut`` and raise this for requests with timeout messaging. - This allows us to be more specific on flaky tests that fail with node timeout errors. Specify ``RequestTimedOut`` as the expected exception in relevant tests marked with ``@pytest.mark.xfail``.
1 parent 4da059c commit 05cee68

File tree

6 files changed

+57
-9
lines changed

6 files changed

+57
-9
lines changed

newsfragments/3440.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implement a ``RequestTimedOut`` exception, extending from ``Web3RPCError``, for when requests to the node time out.

newsfragments/3440.internal.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Mitigate inconsistently failing tests for CI runs with appropriate ``flaky`` or ``pytest.mark.xfail()`` decorators.

tests/core/manager/test_response_formatters.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
BlockNotFound,
1717
ContractLogicError,
1818
MethodUnavailable,
19+
RequestTimedOut,
1920
TransactionNotFound,
2021
Web3RPCError,
2122
)
@@ -77,6 +78,10 @@
7778
VALID_ERROR_RESPONSE,
7879
{"error": {"code": -32601, "message": (METHOD_UNAVAILABLE_MSG)}},
7980
)
81+
ERROR_RESPONSE_REQUEST_TIMED_OUT = merge(
82+
VALID_ERROR_RESPONSE,
83+
{"error": {"code": -32002, "message": "Request timed out."}},
84+
)
8085
ERROR_RESPONSE_INVALID_ID = merge(VALID_ERROR_RESPONSE, {"id": b"invalid"})
8186

8287
ERROR_RESPONSE_INVALID_CODE = merge(VALID_ERROR_RESPONSE, {"error": {"code": "-32601"}})
@@ -190,6 +195,11 @@ def test_formatted_response_invalid_response_object(w3, response, error, error_m
190195
MethodUnavailable,
191196
METHOD_UNAVAILABLE_MSG,
192197
),
198+
(
199+
ERROR_RESPONSE_REQUEST_TIMED_OUT,
200+
RequestTimedOut,
201+
f'{ERROR_RESPONSE_REQUEST_TIMED_OUT["error"]}',
202+
),
193203
),
194204
)
195205
def test_formatted_response_valid_error_object(response, w3, error, error_message):

web3/_utils/module_testing/eth_module.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
MultipleFailedRequests,
7777
NameNotFound,
7878
OffchainLookup,
79+
RequestTimedOut,
7980
TimeExhausted,
8081
TooManyRequests,
8182
TransactionNotFound,
@@ -2359,7 +2360,9 @@ async def test_async_eth_replace_transaction_gas_price_too_low(
23592360
await async_w3.eth.replace_transaction(txn_hash, txn_params)
23602361

23612362
@pytest.mark.xfail(
2362-
reason="Very flaky on CI runs, hard to reproduce locally", strict=False
2363+
reason="Very flaky on CI runs, hard to reproduce locally",
2364+
strict=False,
2365+
raises=(RequestTimedOut, asyncio.TimeoutError, Web3ValueError),
23632366
)
23642367
@pytest.mark.asyncio
23652368
async def test_async_eth_replace_transaction_gas_price_defaulting_minimum(
@@ -2385,7 +2388,9 @@ async def test_async_eth_replace_transaction_gas_price_defaulting_minimum(
23852388
) # minimum gas price
23862389

23872390
@pytest.mark.xfail(
2388-
reason="Very flaky on CI runs, hard to reproduce locally", strict=False
2391+
reason="Very flaky on CI runs, hard to reproduce locally",
2392+
strict=False,
2393+
raises=(RequestTimedOut, asyncio.TimeoutError, Web3ValueError),
23892394
)
23902395
@pytest.mark.asyncio
23912396
async def test_async_eth_replace_transaction_gas_price_defaulting_strategy_higher(
@@ -2416,7 +2421,9 @@ def higher_gas_price_strategy(async_w3: "AsyncWeb3", txn: TxParams) -> Wei:
24162421
async_w3.eth.set_gas_price_strategy(None) # reset strategy
24172422

24182423
@pytest.mark.xfail(
2419-
reason="Very flaky on CI runs, hard to reproduce locally", strict=False
2424+
reason="Very flaky on CI runs, hard to reproduce locally",
2425+
strict=False,
2426+
raises=(RequestTimedOut, asyncio.TimeoutError, Web3ValueError),
24202427
)
24212428
@pytest.mark.asyncio
24222429
async def test_async_eth_replace_transaction_gas_price_defaulting_strategy_lower(

web3/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,12 @@ class MethodUnavailable(Web3RPCError):
364364
"""
365365

366366

367+
class RequestTimedOut(Web3RPCError):
368+
"""
369+
Raised when a request to the node times out.
370+
"""
371+
372+
367373
class TransactionNotFound(Web3RPCError):
368374
"""
369375
Raised when a tx hash used to look up a tx in a jsonrpc call cannot be found.

web3/manager.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
BadResponseFormat,
3838
MethodUnavailable,
3939
ProviderConnectionError,
40+
RequestTimedOut,
4041
TaskNotRunning,
4142
Web3RPCError,
4243
Web3TypeError,
@@ -89,6 +90,13 @@
8990

9091

9192
NULL_RESPONSES = [None, HexBytes("0x"), "0x"]
93+
KNOWN_REQUEST_TIMEOUT_MESSAGING = {
94+
# Note: It's important to be very explicit here and not too broad. We don't want
95+
# to accidentally catch a message that is not for a request timeout. In the worst
96+
# case, we raise something more generic like `Web3RPCError`. JSON-RPC unfortunately
97+
# has not standardized error codes for request timeouts.
98+
"request timed out", # go-ethereum
99+
}
92100
METHOD_NOT_FOUND = -32601
93101

94102

@@ -185,6 +193,7 @@ def _validate_response(
185193
response, 'Response must include either "error" or "result".'
186194
)
187195
elif "error" in response:
196+
web3_rpc_error: Optional[Web3RPCError] = None
188197
error = response["error"]
189198

190199
# raise the error when the value is a string
@@ -202,7 +211,7 @@ def _validate_response(
202211
response, 'error["code"] is required and must be an integer value.'
203212
)
204213
elif code == METHOD_NOT_FOUND:
205-
exception = MethodUnavailable(
214+
web3_rpc_error = MethodUnavailable(
206215
repr(error),
207216
rpc_response=response,
208217
user_message=(
@@ -211,9 +220,6 @@ def _validate_response(
211220
"currently enabled."
212221
),
213222
)
214-
logger.error(exception.user_message)
215-
logger.debug(f"RPC error response: {response}")
216-
raise exception
217223

218224
# errors must include a message
219225
error_message = error.get("message")
@@ -222,9 +228,26 @@ def _validate_response(
222228
response, 'error["message"] is required and must be a string value.'
223229
)
224230

225-
apply_error_formatters(error_formatters, response)
231+
if any(
232+
# parse specific timeout messages
233+
timeout_str in error_message.lower()
234+
for timeout_str in KNOWN_REQUEST_TIMEOUT_MESSAGING
235+
):
236+
web3_rpc_error = RequestTimedOut(
237+
repr(error),
238+
rpc_response=response,
239+
user_message=(
240+
"The request timed out. Check the connection to your node and "
241+
"try again."
242+
),
243+
)
244+
245+
if web3_rpc_error is None:
246+
# if no condition was met above, raise a more generic `Web3RPCError`
247+
web3_rpc_error = Web3RPCError(repr(error), rpc_response=response)
248+
249+
response = apply_error_formatters(error_formatters, response)
226250

227-
web3_rpc_error = Web3RPCError(repr(error), rpc_response=response)
228251
logger.error(web3_rpc_error.user_message)
229252
logger.debug(f"RPC error response: {response}")
230253
raise web3_rpc_error

0 commit comments

Comments
 (0)