Repository: ethereum/web3.py

Issue:
Re-think Revert Reason Parsing 
### What was wrong?

Many nodes/networks return error data in different formats that doesn't comply with the JSON-RPC error spec. `raise_contract_logic_error_on_revert` attempts to parse the errors, and is getting unwieldy. It returns unexpected errors when parsing fails. 


### How can it be fixed?

One option is to check if the error returned matches the JSON-RPC error spec. If it does, parse the error and return the parsed message.  If we go this route, I think we should still keep the `data` attribute on `ContractLogicError` so that users can pass along the raw data to other contracts if they'd like. If the response doesn't match the spec, we'd return some message that indicates that we can't parse it, along with the raw error data. I'm also open to other ideas. 


Discussion:

Patch:
diff --git a/newsfragments/3053.feature.rst b/newsfragments/3053.feature.rst
new file mode 100644
index 00000000..8d23689a
--- /dev/null
+++ b/newsfragments/3053.feature.rst
@@ -0,0 +1,1 @@
+Improved error messaging for exceptions from malformed JSON-RPC responses.
\ No newline at end of file
diff --git a/newsfragments/3097.internal.rst b/newsfragments/3097.internal.rst
new file mode 100644
index 00000000..10f9db93
--- /dev/null
+++ b/newsfragments/3097.internal.rst
@@ -0,0 +1,1 @@
+Ignore flake8 rule F401 (unused import) in all ``__init__.py`` files
diff --git a/tox.ini b/tox.ini
index b03efe00..bdaeec2c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,9 +10,10 @@ envlist=
     benchmark
 
 [flake8]
-max-line-length=88
 exclude= venv*,.tox,docs,build
 extend-ignore=E203,W503
+max-line-length=88
+per-file-ignores=__init__.py:F401
 
 [testenv]
 allowlist_externals=/usr/bin/make
diff --git a/web3/_utils/compat/__init__.py b/web3/_utils/compat/__init__.py
index 01c220a2..38c14e02 100644
--- a/web3/_utils/compat/__init__.py
+++ b/web3/_utils/compat/__init__.py
@@ -8,7 +8,7 @@
 # Once web3 supports >= the noted python version, the type may be directly
 # imported from `typing`
 
-from typing_extensions import (  # noqa: F401
+from typing_extensions import (
     Literal,  # py38
     NotRequired,  # py311
     Protocol,  # py38
diff --git a/web3/_utils/contract_error_handling.py b/web3/_utils/contract_error_handling.py
index ff5dbae1..de492241 100644
--- a/web3/_utils/contract_error_handling.py
+++ b/web3/_utils/contract_error_handling.py
@@ -49,96 +49,118 @@ PANIC_ERROR_CODES = {
     "function type.",
 }
 
+MISSING_DATA = "no data"
 
-def raise_contract_logic_error_on_revert(response: RPCResponse) -> RPCResponse:
-    """
-    Reverts contain a `data` attribute with the following layout:
-        "Reverted "
-        Function selector for Error(string): 08c379a (4 bytes)
-        Data offset: 32 (32 bytes)
-        String length (32 bytes)
-        Reason string (padded, use string length from above to get meaningful part)
 
-    See also https://solidity.readthedocs.io/en/v0.6.3/control-structures.html#revert
+def _parse_error_with_reverted_prefix(data: str) -> str:
     """
-    if not isinstance(response["error"], dict):
-        raise ValueError("Error expected to be a dict")
-
-    data = response["error"].get("data", "")
-    message = response["error"].get("message", "")
-
-    message_present = message is not None and message != ""
+    Parse errors from the data string which begin with the "Reverted" prefix.
+    "Reverted", function selector and offset are always the same for revert errors
+    """
+    prefix = f"Reverted {SOLIDITY_ERROR_FUNC_SELECTOR}"
+    data_offset = ("00" * 31) + "20"  # 0x0000...0020 (32 bytes)
+    revert_pattern = prefix + data_offset
+    error = data
+
+    if data.startswith(revert_pattern):
+        # if common revert pattern
+        string_length = int(data[len(revert_pattern) : len(revert_pattern) + 64], 16)
+        error = data[
+            len(revert_pattern) + 64 : len(revert_pattern) + 64 + string_length * 2
+        ]
+    elif data.startswith("Reverted 0x"):
+        # Special case for this form: 'Reverted 0x...'
+        error = data.split(" ")[1][2:]
+
+    try:
+        error = bytes.fromhex(error).decode("utf8")
+    except UnicodeDecodeError:
+        warnings.warn("Could not decode revert reason as UTF-8", RuntimeWarning)
+        raise ContractLogicError("execution reverted", data=data)
 
-    if data is None:
-        if message_present:
-            raise ContractLogicError(message)
-        elif not message_present:
-            raise ContractLogicError("execution reverted")
-        else:
-            raise Exception("Unreachable")
+    return error
 
-    # Ganache case:
-    if isinstance(data, dict) and message_present:
-        raise ContractLogicError(f"execution reverted: {message}", data=data)
 
-    # Parity/OpenEthereum case:
-    if data.startswith("Reverted "):
-        # "Reverted", function selector and offset are always the same for revert errors
-        prefix = f"Reverted {SOLIDITY_ERROR_FUNC_SELECTOR}"
-        data_offset = ("00" * 31) + "20"  # 0x0000...0020 (32 bytes)
-        revert_pattern = prefix + data_offset
-
-        if data.startswith(revert_pattern):
-            # if common revert pattern
-            string_length = int(
-                data[len(revert_pattern) : len(revert_pattern) + 64], 16
-            )
-            reason_as_hex = data[
-                len(revert_pattern) + 64 : len(revert_pattern) + 64 + string_length * 2
-            ]
-        elif data.startswith("Reverted 0x"):
-            # Special case for this form: 'Reverted 0x...'
-            reason_as_hex = data.split(" ")[1][2:]
-        else:
-            raise ContractLogicError("execution reverted", data=data)
+def _raise_contract_error(response_error_data: str) -> None:
+    """
+    Decode response error from data string and raise appropriate exception.
 
-        try:
-            reason_string = bytes.fromhex(reason_as_hex).decode("utf8")
-            raise ContractLogicError(f"execution reverted: {reason_string}", data=data)
-        except UnicodeDecodeError:
-            warnings.warn("Could not decode revert reason as UTF-8", RuntimeWarning)
-            raise ContractLogicError("execution reverted", data=data)
+        "Reverted " (prefix may be present in `data`)
+        Function selector for Error(string): 08c379a (4 bytes)
+        Data offset: 32 (32 bytes)
+        String length (32 bytes)
+        Reason string (padded, use string length from above to get meaningful part)
+    """
+    if response_error_data.startswith("Reverted "):
+        reason_string = _parse_error_with_reverted_prefix(response_error_data)
+        raise ContractLogicError(
+            f"execution reverted: {reason_string}", data=response_error_data
+        )
 
-    # --- EIP-3668 | CCIP Read --- #
-    if data[:10] == OFFCHAIN_LOOKUP_FUNC_SELECTOR:
-        parsed_data_as_bytes = to_bytes(hexstr=data[10:])
+    elif response_error_data[:10] == OFFCHAIN_LOOKUP_FUNC_SELECTOR:
+        # --- EIP-3668 | CCIP read error --- #
+        parsed_data_as_bytes = to_bytes(hexstr=response_error_data[10:])
         abi_decoded_data = abi.decode(
             list(OFFCHAIN_LOOKUP_FIELDS.values()), parsed_data_as_bytes
         )
         offchain_lookup_payload = dict(
             zip(OFFCHAIN_LOOKUP_FIELDS.keys(), abi_decoded_data)
         )
-        raise OffchainLookup(offchain_lookup_payload, data=data)
+        raise OffchainLookup(offchain_lookup_payload, data=response_error_data)
 
-    # --- Solidity Panic Error --- #
-    if data[:10] == PANIC_ERROR_FUNC_SELECTOR:
-        panic_error_code = data[-2:]
-        raise ContractPanicError(PANIC_ERROR_CODES[panic_error_code], data=data)
+    elif response_error_data[:10] == PANIC_ERROR_FUNC_SELECTOR:
+        # --- Solidity Panic Error --- #
+        panic_error_code = response_error_data[-2:]
+        raise ContractPanicError(
+            PANIC_ERROR_CODES[panic_error_code], data=response_error_data
+        )
 
     # Solidity 0.8.4 introduced custom error messages that allow args to
     # be passed in (or not). See:
     # https://blog.soliditylang.org/2021/04/21/custom-errors/
-    if len(data) >= 10 and not data[:10] == SOLIDITY_ERROR_FUNC_SELECTOR:
+    elif (
+        len(response_error_data) >= 10
+        and not response_error_data[:10] == SOLIDITY_ERROR_FUNC_SELECTOR
+    ):
         # Raise with data as both the message and the data for backwards
         # compatibility and so that data can be accessed via 'data' attribute
         # on the ContractCustomError exception
-        raise ContractCustomError(data, data=data)
+        raise ContractCustomError(response_error_data, data=response_error_data)
 
-    # Geth case:
-    if message_present and response["error"].get("code", "") == 3:
-        raise ContractLogicError(message, data=data)
-    # Geth Revert without error message case:
-    if message_present and "execution reverted" in message:
-        raise ContractLogicError("execution reverted", data=data)
+
+def raise_contract_logic_error_on_revert(response: RPCResponse) -> RPCResponse:
+    """
+    Revert responses contain an error with the following optional attributes:
+        `code` - in this context, used for an unknown edge case when code = '3'
+        `message` - error message is passed to the raised exception
+        `data` - response error details (str, dict, None)
+
+    See also https://solidity.readthedocs.io/en/v0.6.3/control-structures.html#revert
+    """
+    error = response.get("error")
+    if error is None or isinstance(error, str):
+        raise ValueError(error)
+
+    message = error.get("message")
+    message_present = message is not None and message != ""
+    data = error.get("data", MISSING_DATA)
+
+    if data is None:
+        if message_present:
+            raise ContractLogicError(message, data=data)
+        elif not message_present:
+            raise ContractLogicError("execution reverted", data=data)
+    elif isinstance(data, dict) and message_present:
+        raise ContractLogicError(f"execution reverted: {message}", data=data)
+    elif isinstance(data, str):
+        _raise_contract_error(data)
+
+    if message_present:
+        # Geth Revert with error message and code 3 case:
+        if error.get("code") == 3:
+            raise ContractLogicError(message, data=data)
+        # Geth Revert without error message case:
+        elif "execution reverted" in message:
+            raise ContractLogicError("execution reverted", data=data)
 
     return response
diff --git a/web3/beacon/__init__.py b/web3/beacon/__init__.py
index 29964c4b..22ded5c5 100644
--- a/web3/beacon/__init__.py
+++ b/web3/beacon/__init__.py
@@ -1,2 +1,2 @@
-from .async_beacon import AsyncBeacon  # noqa: F401
-from .main import Beacon  # noqa: F401
+from .async_beacon import AsyncBeacon
+from .main import Beacon
diff --git a/web3/contract/__init__.py b/web3/contract/__init__.py
index ed14db7a..11602cf6 100644
--- a/web3/contract/__init__.py
+++ b/web3/contract/__init__.py
@@ -1,8 +1,8 @@
-from web3.contract.async_contract import (  # noqa: F401
+from web3.contract.async_contract import (
     AsyncContract,
     AsyncContractCaller,
 )
-from web3.contract.contract import (  # noqa: F401
+from web3.contract.contract import (
     Contract,
     ContractCaller,
     ContractConstructor,
diff --git a/web3/eth/__init__.py b/web3/eth/__init__.py
index 547d40ca..6b692229 100644
--- a/web3/eth/__init__.py
+++ b/web3/eth/__init__.py
@@ -1,10 +1,10 @@
-from .async_eth import (  # noqa: F401
+from .async_eth import (
     AsyncEth,
 )
-from .base_eth import (  # noqa: F401
+from .base_eth import (
     BaseEth,
 )
-from .eth import (  # noqa: F401
+from .eth import (
     Contract,
     Eth,
 )
diff --git a/web3/manager.py b/web3/manager.py
index 90144029..3e94f30d 100644
--- a/web3/manager.py
+++ b/web3/manager.py
@@ -75,6 +75,19 @@ if TYPE_CHECKING:
 
 
 NULL_RESPONSES = [None, HexBytes("0x"), "0x"]
+METHOD_NOT_FOUND = -32601
+
+
+def _raise_bad_response_format(response: RPCResponse, error: str = "") -> None:
+    message = "The response was in an unexpected format and unable to be parsed."
+    raw_response = f"The raw response is: {response}"
+
+    if error is not None and error != "":
+        message = f"{message} {error}. {raw_response}"
+    else:
+        message = f"{message} {raw_response}"
+
+    raise BadResponseFormat(message)
 
 
 def apply_error_formatters(
@@ -198,6 +211,15 @@ class RequestManager:
         self.logger.debug(f"Making request. Method: {method}")
         return await request_func(method, params)
 
+    #
+    # formatted_response parses and validates JSON-RPC responses for expected
+    # properties (result or an error) with the expected types.
+    #
+    # Required properties are not strictly enforced to further determine which
+    # exception to raise for specific cases.
+    #
+    # See also: https://www.jsonrpc.org/specification
+    #
     @staticmethod
     def formatted_response(
         response: RPCResponse,
@@ -205,36 +227,73 @@ class RequestManager:
         error_formatters: Optional[Callable[..., Any]] = None,
         null_result_formatters: Optional[Callable[..., Any]] = None,
     ) -> Any:
-        if "error" in response:
+        # jsonrpc is not enforced (as per the spec) but if present, it must be 2.0
+        if "jsonrpc" in response and response["jsonrpc"] != "2.0":
+            _raise_bad_response_format(
+                response, 'The "jsonrpc" field must be present with a value of "2.0"'
+            )
+
+        # id is not enforced (as per the spec) but if present, it must be a
+        # string or integer
+        # TODO: v7 - enforce id per the spec
+        if "id" in response:
+            response_id = response["id"]
+            # id is always None for errors
+            if response_id is None and "error" not in response:
+                _raise_bad_response_format(
+                    response, '"id" must be None when an error is present'
+                )
+            elif not isinstance(response_id, (str, int, type(None))):
+                _raise_bad_response_format(response, '"id" must be a string or integer')
+
+        # Response may not include both "error" and "result"
+        if "error" in response and "result" in response:
+            _raise_bad_response_format(
+                response, 'Response cannot include both "error" and "result"'
+            )
+
+        # Format and validate errors
+        elif "error" in response:
+            error = response.get("error")
+            # Raise the error when the value is a string
+            if error is None or isinstance(error, str):
+                raise ValueError(error)
+
+            # Errors must include an integer code
+            code = error.get("code")
+            if not isinstance(code, int):
+                _raise_bad_response_format(response, "error['code'] must be an integer")
+            elif code == METHOD_NOT_FOUND:
+                raise MethodUnavailable(error)
+
+            # Errors must include a message
+            if not isinstance(error.get("message"), str):
+                _raise_bad_response_format(
+                    response, "error['message'] must be a string"
+                )
+
             apply_error_formatters(error_formatters, response)
 
-            # guard against eth-tester case - eth-tester returns a string
-            # with no code, so can't parse what the error is.
-            if isinstance(response["error"], dict):
-                resp_code = response["error"].get("code")
-                if resp_code == -32601:
-                    raise MethodUnavailable(response["error"])
-            raise ValueError(response["error"])
-        # NULL_RESPONSES includes None, so return False here as the default
-        # so we don't apply the null_result_formatters if there is no 'result' key
-        elif response.get("result", False) in NULL_RESPONSES:
-            # null_result_formatters raise either a BlockNotFound
-            # or a TransactionNotFound error, depending on the method called
-            apply_null_result_formatters(null_result_formatters, response, params)
-            return response["result"]
-        elif response.get("result") is not None:
-            return response["result"]
+            raise ValueError(error)
+
+        # Format and validate results
+        elif "result" in response:
+            # Null values for result should apply null_result_formatters
+            # Skip when result not present in the response (fallback to False)
+            if response.get("result", False) in NULL_RESPONSES:
+                apply_null_result_formatters(null_result_formatters, response, params)
+            return response.get("result")
+
+        # Response from eth_subscribe includes response["params"]["result"]
         elif (
-            # eth_subscribe case
             response.get("params") is not None
             and response["params"].get("result") is not None
         ):
             return response["params"]["result"]
+
+        # Any other response type raises BadResponseFormat
         else:
-            raise BadResponseFormat(
-                "The response was in an unexpected format and unable to be parsed. "
-                f"The raw response is: {response}"
-            )
+            _raise_bad_response_format(response)
 
     def request_blocking(
         self,
diff --git a/web3/middleware/__init__.py b/web3/middleware/__init__.py
index 76fc62ed..39697c27 100644
--- a/web3/middleware/__init__.py
+++ b/web3/middleware/__init__.py
@@ -14,22 +14,22 @@ from web3.types import (
     RPCResponse,
 )
 
-from .abi import (  # noqa: F401
+from .abi import (
     abi_middleware,
 )
-from .async_cache import (  # noqa: F401
+from .async_cache import (
     _async_simple_cache_middleware as async_simple_cache_middleware,
     async_construct_simple_cache_middleware,
 )
-from .attrdict import (  # noqa: F401
+from .attrdict import (
     async_attrdict_middleware,
     attrdict_middleware,
 )
-from .buffered_gas_estimate import (  # noqa: F401
+from .buffered_gas_estimate import (
     async_buffered_gas_estimate_middleware,
     buffered_gas_estimate_middleware,
 )
-from .cache import (  # noqa: F401
+from .cache import (
     _latest_block_based_cache_middleware as latest_block_based_cache_middleware,
     _simple_cache_middleware as simple_cache_middleware,
     _time_based_cache_middleware as time_based_cache_middleware,
@@ -37,59 +37,59 @@ from .cache import (  # noqa: F401
     construct_simple_cache_middleware,
     construct_time_based_cache_middleware,
 )
-from .exception_handling import (  # noqa: F401
+from .exception_handling import (
     construct_exception_handler_middleware,
 )
-from .exception_retry_request import (  # noqa: F401
+from .exception_retry_request import (
     async_http_retry_request_middleware,
     http_retry_request_middleware,
 )
-from .filter import (  # noqa: F401
+from .filter import (
     async_local_filter_middleware,
     local_filter_middleware,
 )
-from .fixture import (  # noqa: F401
+from .fixture import (
     async_construct_error_generator_middleware,
     async_construct_result_generator_middleware,
     construct_error_generator_middleware,
     construct_fixture_middleware,
     construct_result_generator_middleware,
 )
-from .formatting import (  # noqa: F401
+from .formatting import (
     construct_formatting_middleware,
 )
-from .gas_price_strategy import (  # noqa: F401
+from .gas_price_strategy import (
     async_gas_price_strategy_middleware,
     gas_price_strategy_middleware,
 )
-from .geth_poa import (  # noqa: F401
+from .geth_poa import (
     async_geth_poa_middleware,
     geth_poa_middleware,
 )
-from .names import (  # noqa: F401
+from .names import (
     async_name_to_address_middleware,
     name_to_address_middleware,
 )
-from .normalize_request_parameters import (  # noqa: F401
+from .normalize_request_parameters import (
     request_parameter_normalizer,
 )
-from .pythonic import (  # noqa: F401
+from .pythonic import (
     pythonic_middleware,
 )
-from .signing import (  # noqa: F401
+from .signing import (
     construct_sign_and_send_raw_middleware,
 )
-from .stalecheck import (  # noqa: F401
+from .stalecheck import (
     async_make_stalecheck_middleware,
     make_stalecheck_middleware,
 )
-from .validation import (  # noqa: F401
+from .validation import (
     async_validation_middleware,
     validation_middleware,
 )
 
 if TYPE_CHECKING:
-    from web3 import AsyncWeb3, Web3  # noqa: F401
+    from web3 import AsyncWeb3, Web3
 
 
 def combine_middlewares(
diff --git a/web3/providers/__init__.py b/web3/providers/__init__.py
index 662a8e5c..0ad0d624 100644
--- a/web3/providers/__init__.py
+++ b/web3/providers/__init__.py
@@ -1,26 +1,26 @@
-from .async_base import (  # noqa: F401
+from .async_base import (
     AsyncBaseProvider,
 )
-from .async_rpc import (  # noqa: F401
+from .async_rpc import (
     AsyncHTTPProvider,
 )
-from .base import (  # noqa: F401
+from .base import (
     BaseProvider,
     JSONBaseProvider,
 )
-from .ipc import (  # noqa: F401
+from .ipc import (
     IPCProvider,
 )
-from .persistent import (  # noqa: F401
+from .persistent import (
     PersistentConnectionProvider,
 )
-from .rpc import (  # noqa: F401
+from .rpc import (
     HTTPProvider,
 )
-from .websocket import (  # noqa: F401
+from .websocket import (
     WebsocketProvider,
     WebsocketProviderV2,
 )
-from .auto import (  # noqa: F401
+from .auto import (
     AutoProvider,
 )
diff --git a/web3/providers/websocket/__init__.py b/web3/providers/websocket/__init__.py
index 56e8ea03..daf0ad63 100644
--- a/web3/providers/websocket/__init__.py
+++ b/web3/providers/websocket/__init__.py
@@ -1,8 +1,8 @@
-from .websocket import (  # noqa: F401
+from .websocket import (
     DEFAULT_WEBSOCKET_TIMEOUT,
     RESTRICTED_WEBSOCKET_KWARGS,
     WebsocketProvider,
 )
-from .websocket_v2 import (  # noqa: F401
+from .websocket_v2 import (
     WebsocketProviderV2,
 )
diff --git a/web3/tools/__init__.py b/web3/tools/__init__.py
index 01cd73a9..7cc14d91 100644
--- a/web3/tools/__init__.py
+++ b/web3/tools/__init__.py
@@ -1,4 +1,4 @@
-from .pytest_ethereum import (  # noqa: F401
+from .pytest_ethereum import (
     deployer,
     linker,
 )
diff --git a/web3/types.py b/web3/types.py
index 67042b09..5e234434 100644
--- a/web3/types.py
+++ b/web3/types.py
@@ -283,7 +283,7 @@ class GethSyncingSubscriptionResponse(SubscriptionResponse):
 
 class RPCResponse(TypedDict, total=False):
     error: Union[RPCError, str]
-    id: int
+    id: Optional[Union[int, str]]
     jsonrpc: Literal["2.0"]
     result: Any
 


Test patch:
diff --git a/tests/core/manager/test_response_formatters.py b/tests/core/manager/test_response_formatters.py
index 1daedd8d..c6afb395 100644
--- a/tests/core/manager/test_response_formatters.py
+++ b/tests/core/manager/test_response_formatters.py
@@ -1,4 +1,5 @@
 import pytest
+import re
 
 from eth_utils.toolz import (
     identity,
@@ -25,9 +26,36 @@ ERROR_RESPONSE = {
         "because the ancient block sync is still in progress.",
     },
 }
+ERROR_RESPONSE_WITH_NONE_ID = {
+    "id": None,
+    "jsonrpc": "2.0",
+    "error": {
+        "code": -32000,
+        "message": "Requested block number is in a range that is not available yet, "
+        "because the ancient block sync is still in progress.",
+    },
+}
+ERROR_RESPONSE_WITH_VALID_ID = {
+    "id": 1,
+    "jsonrpc": "2.0",
+    "error": {
+        "code": -32000,
+        "message": "Requested block number is in a range that is not available yet, "
+        "because the ancient block sync is still in progress.",
+    },
+}
 NONE_RESPONSE = {"jsonrpc": "2.0", "id": 1, "result": None}
 ZERO_X_RESPONSE = {"jsonrpc": "2.0", "id": 1, "result": "0x"}
+INVALID_JSONRPC_RESP_FORMAT = {
+    "jsonrpc": "999",
+    "error": {
+        "code": -32000,
+        "message": "Requested block number is in a range that is not available yet, "
+        "because the ancient block sync is still in progress.",
+    },
+}
 UNEXPECTED_RESPONSE_FORMAT = {"jsonrpc": "2.0", "id": 1}
+UNEXPECTED_RESPONSE_FORMAT_NONE_ID = {"jsonrpc": "2.0", "id": None}
 ANOTHER_UNEXPECTED_RESP_FORMAT = {
     "name": "LimitError",
     "message": "You cannot query logs for more than 10000 blocks at once.",
@@ -41,6 +69,28 @@ METHOD_NOT_FOUND_RESP_FORMAT = {
         "available",
     },
 }
+INVALID_CODE_RESP_FORMAT = {
+    "jsonrpc": "2.0",
+    "error": {
+        "code": "-32601",
+        "message": "the method eth_getTransactionByHash does not exist/is not "
+        "available",
+    },
+}
+MISSING_CODE_RESP_FORMAT = {
+    "jsonrpc": "2.0",
+    "error": {
+        "message": "the method eth_getTransactionByHash does not exist/is not "
+        "available",
+    },
+}
+INVALID_MESSAGE_RESP_FORMAT = {
+    "jsonrpc": "2.0",
+    "error": {
+        "code": -32000,
+        "message": {},
+    },
+}
 ETH_TESTER_METHOD_NOT_FOUND_RESP_FORMAT = {
     "error": "the method eth_getTransactionByHash does not exist/is not available",
 }
@@ -77,6 +127,22 @@ def raise_contract_logic_error(response):
             raise_block_not_found,
             ValueError,
         ),
+        (
+            # Error response with no result formatters raises a ValueError
+            ERROR_RESPONSE_WITH_NONE_ID,
+            (),
+            identity,
+            raise_block_not_found,
+            ValueError,
+        ),
+        (
+            # Error response with no result formatters raises a ValueError
+            ERROR_RESPONSE_WITH_VALID_ID,
+            (),
+            identity,
+            raise_block_not_found,
+            ValueError,
+        ),
         (
             # None result raises error if there is a null_result_formatter
             NONE_RESPONSE,
@@ -169,13 +235,24 @@ def test_formatted_response_raises_errors(
             TransactionNotFound,
             "Transaction with hash: '0x01' not found.",
         ),
+        (
+            INVALID_JSONRPC_RESP_FORMAT,
+            (),
+            identity,
+            identity,
+            BadResponseFormat,
+            f"The response was in an unexpected format and unable to be parsed. "
+            f'The "jsonrpc" field must be present with a value of "2.0". '
+            f"The raw response is: {INVALID_JSONRPC_RESP_FORMAT}",
+        ),
         (
             UNEXPECTED_RESPONSE_FORMAT,
             (),
             identity,
             identity,
             BadResponseFormat,
-            f"The response was in an unexpected format and unable to be parsed. The raw response is: {UNEXPECTED_RESPONSE_FORMAT}",  # noqa: E501
+            f"The response was in an unexpected format and unable to be parsed. "
+            f"The raw response is: {UNEXPECTED_RESPONSE_FORMAT}",
         ),
         (
             ANOTHER_UNEXPECTED_RESP_FORMAT,
@@ -183,7 +260,44 @@ def test_formatted_response_raises_errors(
             identity,
             identity,
             BadResponseFormat,
-            f"The response was in an unexpected format and unable to be parsed. The raw response is: {ANOTHER_UNEXPECTED_RESP_FORMAT}",  # noqa: E501
+            f"The response was in an unexpected format and unable to be parsed. "
+            f"The raw response is: {ANOTHER_UNEXPECTED_RESP_FORMAT}",
+        ),
+        (
+            INVALID_CODE_RESP_FORMAT,
+            (),
+            identity,
+            identity,
+            BadResponseFormat,
+            re.escape(
+                f"The response was in an unexpected format and unable to be parsed. "
+                f"error['code'] must be an integer. "
+                f"The raw response is: {INVALID_CODE_RESP_FORMAT}"
+            ),
+        ),
+        (
+            MISSING_CODE_RESP_FORMAT,
+            (),
+            identity,
+            identity,
+            BadResponseFormat,
+            re.escape(
+                f"The response was in an unexpected format and unable to be parsed. "
+                f"error['code'] must be an integer. "
+                f"The raw response is: {MISSING_CODE_RESP_FORMAT}"
+            ),
+        ),
+        (
+            INVALID_MESSAGE_RESP_FORMAT,
+            (),
+            identity,
+            identity,
+            BadResponseFormat,
+            re.escape(
+                f"The response was in an unexpected format and unable to be parsed. "
+                f"error['message'] must be a string. "
+                f"The raw response is: {INVALID_MESSAGE_RESP_FORMAT}"
+            ),
         ),
     ],
 )
diff --git a/web3/_utils/module_testing/__init__.py b/web3/_utils/module_testing/__init__.py
index 2f248834..c39d7422 100644
--- a/web3/_utils/module_testing/__init__.py
+++ b/web3/_utils/module_testing/__init__.py
@@ -1,21 +1,21 @@
-from .eth_module import (  # noqa: F401
+from .eth_module import (
     AsyncEthModuleTest,
     EthModuleTest,
 )
-from .go_ethereum_admin_module import (  # noqa: F401
+from .go_ethereum_admin_module import (
     GoEthereumAdminModuleTest,
 )
-from .go_ethereum_personal_module import (  # noqa: F401
+from .go_ethereum_personal_module import (
     GoEthereumPersonalModuleTest,
 )
-from .go_ethereum_txpool_module import (  # noqa: F401
+from .go_ethereum_txpool_module import (
     GoEthereumAsyncTxPoolModuleTest,
     GoEthereumTxPoolModuleTest,
 )
-from .net_module import (  # noqa: F401
+from .net_module import (
     AsyncNetModuleTest,
     NetModuleTest,
 )
-from .web3_module import (  # noqa: F401
+from .web3_module import (
     Web3ModuleTest,
 )
diff --git a/web3/providers/eth_tester/__init__.py b/web3/providers/eth_tester/__init__.py
index 2892cb51..dd3c39a0 100644
--- a/web3/providers/eth_tester/__init__.py
+++ b/web3/providers/eth_tester/__init__.py
@@ -1,4 +1,4 @@
-from .main import (  # noqa: F401
+from .main import (
     AsyncEthereumTesterProvider,
     EthereumTesterProvider,
 )