Repository: aio-libs/aiohttp

Issue:
ClientResponse.get_encoding() throws TypeError if it hasn't been read 
## Long story short
`ClientResponse.get_encoding()` throws `TypeError: object of type 'NoneType' has no len()` if we don't first call `text()` or `json()` to read from it, i.e. `_body is None`

Related: #4112 

<!-- Please describe your problem and why the fix is important. -->

## Expected behaviour
Does not throw an error, returns some default value
<!-- What is the behaviour you expect? -->

## Actual behaviour
```python-traceback
Traceback (most recent call last):
  File "/home/transfusion/aiohttp/aiohttp_issue_4112.py", line 14, in <module>
    loop.run_until_complete(test())
  File "/usr/lib/python3.7/asyncio/base_events.py", line 579, in run_until_complete
    return future.result()
  File "/home/transfusion/aiohttp/aiohttp_issue_4112.py", line 10, in test
    print(r.get_encoding())
  File "/home/transfusion/aiohttp/aiohttp/client_reqrep.py", line 942, in get_encoding
    encoding = chardet.detect(self._body)['encoding']
  File "/home/transfusion/python37/lib/python3.7/site-packages/cchardet/__init__.py", line 15, in detect
    encoding, confidence = _cchardet.detect_with_confidence(msg)
  File "src/cchardet/_cchardet.pyx", line 15, in cchardet._cchardet.detect_with_confidence
TypeError: object of type 'NoneType' has no len()
```
## Steps to reproduce

Find any webpage that doesn't include a `charset` parameter in its `Content-Type` response header; you may check headers only with `curl -v -s http://example.com 1> /dev/null`

```py
import aiohttp
import asyncio


async def test():

    async with aiohttp.ClientSession() as session:
        r = await session.get('http://www.africau.edu', ssl=None)
        # print(await r.text())
        print(r.get_encoding())

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(test())
```

## Your environment

4.0.0a1, Commit f356eeb8ccefd1e176536d396d026a23da1f1695



Discussion:
I would raise an explicit error here instead of returning a default.
`raise RuntimeError(...)` with an explanation text message would be good.
Can you propose a pull request?
Hello, does this issue still actual? I'd like to propose PR

Resolving patch:
diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py
--- a/aiohttp/client_reqrep.py
+++ b/aiohttp/client_reqrep.py
@@ -938,6 +938,9 @@ def get_encoding(self) -> str:
             if mimetype.type == 'application' and mimetype.subtype == 'json':
                 # RFC 7159 states that the default encoding is UTF-8.
                 encoding = 'utf-8'
+            elif self._body is None:
+                raise RuntimeError('Cannot guess the encoding of '
+                                   'a not yet read body')
             else:
                 encoding = chardet.detect(self._body)['encoding']
         if not encoding:


Test patch:
diff --git a/tests/test_client_response.py b/tests/test_client_response.py
--- a/tests/test_client_response.py
+++ b/tests/test_client_response.py
@@ -424,6 +424,33 @@ def side_effect(*args, **kwargs):
     assert response.get_encoding().lower() in ('windows-1251', 'maccyrillic')
 
 
+async def test_get_encoding_body_none(loop, session) -> None:
+    response = ClientResponse('get', URL('http://def-cl-resp.org'),
+                              request_info=mock.Mock(),
+                              writer=mock.Mock(),
+                              continue100=None,
+                              timer=TimerNoop(),
+                              traces=[],
+                              loop=loop,
+                              session=session)
+
+    def side_effect(*args, **kwargs):
+        fut = loop.create_future()
+        fut.set_result('{"encoding": "test"}')
+        return fut
+
+    response._headers = {'Content-Type': 'text/html'}
+    content = response.content = mock.Mock()
+    content.read.side_effect = side_effect
+
+    with pytest.raises(
+        RuntimeError,
+        match='^Cannot guess the encoding of a not yet read body$',
+    ):
+        response.get_encoding()
+    assert response.closed
+
+
 async def test_text_after_read(loop, session) -> None:
     response = ClientResponse('get', URL('http://def-cl-resp.org'),
                               request_info=mock.Mock(),