Repository: ethereum/web3.py

Issue:
Bug with `geth_poa_middleware` in the case of non-existing blocks
* Version: 5.20.1
* Python: 3.7.2
* OS: linux

`pip freeze` output:

```
aiohttp==3.7.4.post0
async-timeout==3.0.1
attrs==20.3.0
base58==2.1.0
bitarray==1.2.2
certifi==2020.12.5
chardet==4.0.0
cytoolz==0.11.0
docutils==0.17.1
eth-abi==2.1.1
eth-account==0.5.4
eth-hash==0.3.1
eth-keyfile==0.5.1
eth-keys==0.3.3
eth-rlp==0.2.1
eth-typing==2.2.2
eth-utils==1.10.0
flock==0.1
hexbytes==0.2.1
idna==2.10
importlib-metadata==4.0.1
ipfshttpclient==0.7.0
jsonschema==3.2.0
lockfile==0.12.2
lru-dict==1.1.7
multiaddr==0.0.9
multidict==5.1.0
netaddr==0.8.0
parsimonious==0.8.1
protobuf==3.15.8
pycryptodome==3.10.1
pyrsistent==0.17.3
python-daemon==2.3.0
PyYAML==5.4.1
requests==2.25.1
rlp==2.0.1
six==1.15.0
toolz==0.11.1
typing-extensions==3.7.4.3
urllib3==1.26.4
varint==1.0.2
web3==5.20.1
websockets==9.1
yarl==1.6.3
zipp==3.4.1
```


### What was wrong?

The poa middleware formatters are applied to block contents even in the case when block is not found and `None` returned instead.

Reproducing:

```python
from eth_typing import URI
from web3 import Web3
from web3.middleware import geth_poa_middleware
from web3.providers.auto import load_provider_from_uri

w3 = Web3(load_provider_from_uri(URI('wss://rinkeby.infura.io/ws/v3/__project_id_here__'))) # please update the endpoint url
w3.middleware_onion.inject(geth_poa_middleware, layer=0)
print(w3.eth.get_block(200000000))
```

Expecting `web3.exceptions.BlockNotFound` to be risen.

Instead, got:

```
Traceback (most recent call last):
  File "./bin/feed-query", line 85, in <module>
    main()
  File "./bin/feed-query", line 39, in main
    print(w3.eth.get_block(200000000))
  File "/venv/lib/python3.7/site-packages/web3/module.py", line 58, in caller
    result = w3.manager.request_blocking(method_str, params, error_formatters)
  File "/venv/lib/python3.7/site-packages/web3/manager.py", line 154, in request_blocking
    response = self._make_request(method, params)
  File "/venv/lib/python3.7/site-packages/web3/manager.py", line 133, in _make_request
    return request_func(method, params)
  File "cytoolz/functoolz.pyx", line 250, in cytoolz.functoolz.curry.__call__
  File "/venv/lib/python3.7/site-packages/web3/middleware/formatting.py", line 76, in apply_formatters
    response = make_request(method, params)
  File "/venv/lib/python3.7/site-packages/web3/middleware/gas_price_strategy.py", line 34, in middleware
    return make_request(method, params)
  File "cytoolz/functoolz.pyx", line 250, in cytoolz.functoolz.curry.__call__
  File "/venv/lib/python3.7/site-packages/web3/middleware/formatting.py", line 76, in apply_formatters
    response = make_request(method, params)
  File "/venv/lib/python3.7/site-packages/web3/middleware/attrdict.py", line 33, in middleware
    response = make_request(method, params)
  File "cytoolz/functoolz.pyx", line 250, in cytoolz.functoolz.curry.__call__
  File "/venv/lib/python3.7/site-packages/web3/middleware/formatting.py", line 74, in apply_formatters
    response = make_request(method, formatted_params)
  File "/venv/lib/python3.7/site-packages/web3/middleware/normalize_errors.py", line 25, in middleware
    result = make_request(method, params)
  File "cytoolz/functoolz.pyx", line 250, in cytoolz.functoolz.curry.__call__
  File "/venv/lib/python3.7/site-packages/web3/middleware/formatting.py", line 76, in apply_formatters
    response = make_request(method, params)
  File "cytoolz/functoolz.pyx", line 250, in cytoolz.functoolz.curry.__call__
  File "/venv/lib/python3.7/site-packages/web3/middleware/formatting.py", line 76, in apply_formatters
    response = make_request(method, params)
  File "cytoolz/functoolz.pyx", line 250, in cytoolz.functoolz.curry.__call__
  File "/venv/lib/python3.7/site-packages/web3/middleware/formatting.py", line 83, in apply_formatters
    formatter(response["result"]),
  File "cytoolz/functoolz.pyx", line 503, in cytoolz.functoolz.Compose.__call__
  File "cytoolz/functoolz.pyx", line 250, in cytoolz.functoolz.curry.__call__
  File "/venv/lib/python3.7/site-packages/eth_utils/functional.py", line 45, in inner
    return callback(fn(*args, **kwargs))
  File "/venv/lib/python3.7/site-packages/eth_utils/applicators.py", line 126, in apply_key_map
    set(value.keys())
AttributeError: 'NoneType' object has no attribute 'keys'
```

### How can it be fixed?

A simple monkey-patch that works for me:

```
def _geth_poa_middleware():
    from web3.middleware.geth_poa import geth_poa_cleanup
    from web3.middleware import construct_formatting_middleware
    from web3.types import RPCEndpoint

    def m(value):
        # eth_getBlock* may return None
        return None if value is None else geth_poa_cleanup(value)

    return construct_formatting_middleware(
        result_formatters={
            RPCEndpoint("eth_getBlockByHash"): m,
            RPCEndpoint("eth_getBlockByNumber"): m,
        },
    )
```

Could be easily incorporated into `web3.middleware.geth_poa` IMO.



Discussion:

Resolving patch:
diff --git a/web3/middleware/geth_poa.py b/web3/middleware/geth_poa.py
--- a/web3/middleware/geth_poa.py
+++ b/web3/middleware/geth_poa.py
@@ -1,20 +1,25 @@
 from eth_utils.curried import (
+    apply_formatter_if,
     apply_formatters_to_dict,
     apply_key_map,
+    is_null,
 )
 from eth_utils.toolz import (
+    complement,
     compose,
 )
 from hexbytes import (
     HexBytes,
 )
 
+from web3._utils.rpc_abi import (
+    RPC,
+)
 from web3.middleware.formatting import (
     construct_formatting_middleware,
 )
-from web3.types import (
-    RPCEndpoint,
-)
+
+is_not_null = complement(is_null)
 
 remap_geth_poa_fields = apply_key_map({
     'extraData': 'proofOfAuthorityData',
@@ -28,7 +33,7 @@
 
 geth_poa_middleware = construct_formatting_middleware(
     result_formatters={
-        RPCEndpoint("eth_getBlockByHash"): geth_poa_cleanup,
-        RPCEndpoint("eth_getBlockByNumber"): geth_poa_cleanup,
+        RPC.eth_getBlockByHash: apply_formatter_if(is_not_null, geth_poa_cleanup),
+        RPC.eth_getBlockByNumber: apply_formatter_if(is_not_null, geth_poa_cleanup),
     },
 )


Test patch:
diff --git a/tests/core/eth-module/test_poa.py b/tests/core/eth-module/test_poa.py
--- a/tests/core/eth-module/test_poa.py
+++ b/tests/core/eth-module/test_poa.py
@@ -1,6 +1,7 @@
 import pytest
 
 from web3.exceptions import (
+    BlockNotFound,
     ExtraDataLengthError,
 )
 from web3.middleware import (
@@ -37,3 +38,13 @@ def test_geth_proof_of_authority(web3):
     block = web3.eth.get_block('latest')
     assert 'extraData' not in block
     assert block.proofOfAuthorityData == b'\xff' * 33
+
+
+def test_returns_none_response(web3):
+    return_none_response = construct_fixture_middleware({
+        'eth_getBlockByNumber': None,
+    })
+    web3.middleware_onion.inject(geth_poa_middleware, layer=0)
+    web3.middleware_onion.inject(return_none_response, layer=0)
+    with pytest.raises(BlockNotFound):
+        web3.eth.get_block(100000000000)