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

Stricter validation for JSON-RPC responses #3359

Merged
merged 8 commits into from
Apr 24, 2024
37 changes: 37 additions & 0 deletions docs/v7_migration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,41 @@ keys in the dictionary should be camelCase. This is because the dictionary is pa
directly to the JSON-RPC request, where the keys are expected to be in camelCase.


Changes to Exception Handling
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

All Python standard library exceptions that were raised from within web3.py have
been replaced with custom ``Web3Exception`` classes. This change allows for better
control over exception handling, being able to distinguish between exceptions raised
by web3.py and those raised from elsewhere in a codebase. The following exceptions
have been replaced:

- ``AssertionError`` -> ``Web3AssertionError``
- ``ValueError`` -> ``Web3ValueError``
- ``TypeError`` -> ``Web3TypeError``
- ``AttributeError`` -> ``Web3AttributeError``

A new ``MethodNotSupported`` exception is now raised when a method is not supported by
web3.py. This allows a user to distinguish between when a method is not available on
the current provider, ``MethodUnavailable``, and when a method is not supported by
web3.py under certain conditions, ``MethodNotSupported``.


JSON-RPC Error Handling
```````````````````````

Rather than a ``ValueError`` being replaced with a ``Web3ValueError`` when a JSON-RPC
response comes back with an ``error`` object, a new ``Web3RPCError`` exception is
now raised to provide more distinction for JSON-RPC error responses. Some previously
existing exceptions now extend from this class since they too are related to JSON-RPC
errors:

- ``MethodUnavailable``
- ``BlockNotFound``
- ``TransactionNotFound``
- ``TransactionIndexingInProgress``


Miscellaneous Changes
~~~~~~~~~~~~~~~~~~~~~

Expand All @@ -207,3 +242,5 @@ Miscellaneous Changes
without checking if the ``geth.ipc`` file exists.
- ``Web3.is_address()`` returns ``True`` for non-checksummed addresses.
- ``Contract.encodeABI()`` has been renamed to ``Contract.encode_abi()``.
- JSON-RPC responses are now more strictly validated against the JSON-RPC 2.0
specification while providing more informative error messages for invalid responses.
1 change: 1 addition & 0 deletions newsfragments/3359.breaking.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Validate JSON-RPC responses more strictly against the JSON-RPC 2.0 specifications. ``BlockNumberOutofRange`` -> ``BlockNumberOutOfRange``.
1 change: 1 addition & 0 deletions newsfragments/3359.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Raise ``Web3RPCError`` on JSON-RPC errors rather than ``Web3ValueError``. Raise ``MethodNotSupported`` exception when a method is not supported within *web3.py*; keep ``MethodUnavailable`` for when a method is not available on the current provider (JSON-RPC error).
12 changes: 6 additions & 6 deletions tests/core/caching-utils/test_request_caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
generate_cache_key,
)
from web3.exceptions import (
Web3ValueError,
Web3RPCError,
)
from web3.providers import (
AsyncBaseProvider,
Expand All @@ -31,7 +31,7 @@ def simple_cache_return_value_a():
_cache = SimpleCache()
_cache.cache(
generate_cache_key(f"{threading.get_ident()}:{('fake_endpoint', [1])}"),
{"result": "value-a"},
{"jsonrpc": "2.0", "id": 0, "result": "value-a"},
)
return _cache

Expand Down Expand Up @@ -92,9 +92,9 @@ def test_request_caching_does_not_cache_error_responses(request_mocker):
with request_mocker(
w3, mock_errors={"fake_endpoint": lambda *_: {"message": f"msg-{uuid.uuid4()}"}}
):
with pytest.raises(Web3ValueError) as err_a:
with pytest.raises(Web3RPCError) as err_a:
w3.manager.request_blocking("fake_endpoint", [])
with pytest.raises(Web3ValueError) as err_b:
with pytest.raises(Web3RPCError) as err_b:
w3.manager.request_blocking("fake_endpoint", [])

assert str(err_a) != str(err_b)
Expand Down Expand Up @@ -197,9 +197,9 @@ async def test_async_request_caching_does_not_cache_error_responses(request_mock
async_w3,
mock_errors={"fake_endpoint": lambda *_: {"message": f"msg-{uuid.uuid4()}"}},
):
with pytest.raises(Web3ValueError) as err_a:
with pytest.raises(Web3RPCError) as err_a:
await async_w3.manager.coro_request("fake_endpoint", [])
with pytest.raises(Web3ValueError) as err_b:
with pytest.raises(Web3RPCError) as err_b:
await async_w3.manager.coro_request("fake_endpoint", [])

assert str(err_a) != str(err_b)
Expand Down
6 changes: 3 additions & 3 deletions tests/core/contracts/test_contract_call_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
)
from web3.exceptions import (
BadFunctionCallOutput,
BlockNumberOutofRange,
BlockNumberOutOfRange,
FallbackNotFound,
InvalidAddress,
MismatchedABI,
Expand Down Expand Up @@ -538,7 +538,7 @@ def test_call_nonexistent_receive_function(fallback_function_contract):

def test_throws_error_if_block_out_of_range(w3, math_contract):
w3.provider.make_request(method="evm_mine", params=[20])
with pytest.raises(BlockNumberOutofRange):
with pytest.raises(BlockNumberOutOfRange):
math_contract.functions.counter().call(block_identifier=-50)


Expand Down Expand Up @@ -1726,7 +1726,7 @@ async def test_async_call_nonexistent_receive_function(
@pytest.mark.asyncio
async def test_async_throws_error_if_block_out_of_range(async_w3, async_math_contract):
await async_w3.provider.make_request(method="evm_mine", params=[20])
with pytest.raises(BlockNumberOutofRange):
with pytest.raises(BlockNumberOutOfRange):
await async_math_contract.functions.counter().call(block_identifier=-50)


Expand Down
10 changes: 5 additions & 5 deletions tests/core/contracts/test_contract_caller_interface.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest

from web3.exceptions import (
BlockNumberOutofRange,
BlockNumberOutOfRange,
MismatchedABI,
NoABIFound,
NoABIFunctionsFound,
Expand Down Expand Up @@ -139,12 +139,12 @@ def test_caller_with_invalid_block_identifier(w3, math_contract):
# Invalid `block_identifier` should not raise when passed to `caller` directly.
# The function call itself parses the value.
caller = math_contract.caller(block_identifier="abc")
with pytest.raises(BlockNumberOutofRange):
with pytest.raises(BlockNumberOutOfRange):
caller.counter()

# Calling the function with an invalid `block_identifier` will raise.
default_caller = math_contract.caller()
with pytest.raises(BlockNumberOutofRange):
with pytest.raises(BlockNumberOutOfRange):
default_caller.counter(block_identifier="abc")


Expand Down Expand Up @@ -347,12 +347,12 @@ async def test_async_caller_with_invalid_block_identifier(
# Invalid `block_identifier` should not raise when passed to `caller` directly.
# The function call itself parses the value.
caller = async_math_contract.caller(block_identifier="abc")
with pytest.raises(BlockNumberOutofRange):
with pytest.raises(BlockNumberOutOfRange):
await caller.counter()

# Calling the function with an invalid `block_identifier` will raise.
default_caller = async_math_contract.caller()
with pytest.raises(BlockNumberOutofRange):
with pytest.raises(BlockNumberOutOfRange):
await default_caller.counter(block_identifier="abc")


Expand Down
6 changes: 3 additions & 3 deletions tests/core/contracts/test_contract_util_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
validate_payable,
)
from web3.exceptions import (
BlockNumberOutofRange,
BlockNumberOutOfRange,
)


Expand Down Expand Up @@ -50,7 +50,7 @@ def test_parse_block_identifier_bytes_and_hex(w3):
),
)
def test_parse_block_identifier_error(w3, block_identifier):
with pytest.raises(BlockNumberOutofRange):
with pytest.raises(BlockNumberOutOfRange):
parse_block_identifier(w3, block_identifier)


Expand Down Expand Up @@ -117,7 +117,7 @@ async def test_async_parse_block_identifier_bytes_and_hex(async_w3):
),
)
async def test_async_parse_block_identifier_error(async_w3, block_identifier):
with pytest.raises(BlockNumberOutofRange):
with pytest.raises(BlockNumberOutOfRange):
await async_parse_block_identifier(async_w3, block_identifier)


Expand Down
2 changes: 1 addition & 1 deletion tests/core/manager/test_middleware_can_be_stateful.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class StatefulMiddleware(Web3Middleware):
def wrap_make_request(self, make_request):
def middleware(method, params):
self.state.append((method, params))
return {"result": self.state}
return {"jsonrpc": "2.0", "id": 1, "result": self.state}

return middleware

Expand Down
2 changes: 2 additions & 0 deletions tests/core/manager/test_provider_request_wrapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
class DummyProvider(BaseProvider):
def make_request(self, method, params):
return {
"jsonrpc": "2.0",
"id": 1,
"result": {
"method": method,
"params": params,
Expand Down