Repository: aio-libs/aiohttp

Issue:
Can't read response content after checking response.ok
🐞 **Describe the bug**
For failed requests, ClientResponse is "released" after calling to `resposne.ok` and therefore can't read `response.text()` after checking `response.ok`

💡 **To Reproduce**
```python
async with session.get("url-returning-400+") as response:
    if not response.ok:
       msg = await response.text() # raises ClientConnectionError('Connection closed')
```
does not work, while
```python
async with session.get("url-returning-400+") as response:
    if response.status != 200:
        msg = await response.text() # works just fine
```
works just fine

💡 **Expected behavior**
I'm not sure this is expected behaviour. `response.ok` is read-only property and I believe it should not change the state of the response object. Also, failed requests may contain response body, e.g. with error description, so I'm expecting `await response.text()` to work properly after checking `response.ok`

📋 **Your version of the Python**
<!-- Attach your version of the Python. -->
```console
$ python --version
Python 3.8.6
```

📋 **Your version of the aiohttp/yarl/multidict distributions**
<!-- Attach your version of the distributions in the code blocks below. -->
```console
$ python -m pip show aiohttp
Name: aiohttp
Version: 3.7.3
Summary: Async http client/server framework (asyncio)
Home-page: https://github.com/aio-libs/aiohttp
Author: Nikolay Kim
Author-email: fafhrd91@gmail.com
License: Apache 2
Location: /Users/I028910/sbn/job-context-store/.venv/lib/python3.8/site-packages
Requires: async-timeout, attrs, typing-extensions, chardet, yarl, multidict
Required-by: sbn-aio-utils, aioresponses, aiohttp-retry
```
```console
$ python -m pip show multidict
Name: multidict
Version: 5.1.0
Summary: multidict implementation
Home-page: https://github.com/aio-libs/multidict
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache 2
Location: /Users/I028910/sbn/job-context-store/.venv/lib/python3.8/site-packages
Requires: 
Required-by: yarl, aiohttp
```
```console
$ python -m pip show yarl
Name: yarl
Version: 1.6.3
Summary: Yet another URL library
Home-page: https://github.com/aio-libs/yarl/
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache 2
Location: /Users/I028910/sbn/job-context-store/.venv/lib/python3.8/site-packages
Requires: idna, multidict
Required-by: aiohttp
```



Discussion:
> ```python
> await msg = response.text()
> ```

This syntax is invalid. Instead, it should be:
```python
msg = await response.text()
```
Looking at the implementation, I think this is expected behaviour.

Typical usage is:
```
if resp.ok:
    msg = await resp.text()
```

The expectation appears to be that you would not want to read anything from the connection if `resp.ok` is `False`. i.e. It releases when the resp is an error, not when it's successful.
Hmm, appears to be a fix for this issue: https://github.com/aio-libs/aiohttp/issues/3364

So, maybe it shouldn't apply to resp.ok?
`res.ok` calls `resp.raise_for_status` which in turn calls `resp.release()` in case of errors, marking the connection as wasted: https://github.com/aio-libs/aiohttp/blame/025d940/aiohttp/client_reqrep.py#L936

I think this is indeed a bug. The best way to fix it is to put the `400 <= self.status` check into the `ok` property and then, we could use `self.ok` in the check that `raise_for_status()` has. So it'd work vice versa.

> The expectation appears to be that you would not want to read anything from the connection if `resp.ok` is `False`.

I think it's not an expectation but something that's been overlooked during the review of https://github.com/aio-libs/aiohttp/pull/4711.

cc @EvanKepner
> > ```python
> > await msg = response.text()
> > ```
> 
> This syntax is invalid. Instead, it should be:
> 
> ```python
> msg = await response.text()
> ```

Thanks, updated, that was just "stupid copy/paste" :)

Resolving patch:
diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py
--- a/aiohttp/client_reqrep.py
+++ b/aiohttp/client_reqrep.py
@@ -923,14 +923,10 @@ def ok(self) -> bool:
         This is **not** a check for ``200 OK`` but a check that the response
         status is under 400.
         """
-        try:
-            self.raise_for_status()
-        except ClientResponseError:
-            return False
-        return True
+        return 400 > self.status
 
     def raise_for_status(self) -> None:
-        if 400 <= self.status:
+        if not self.ok:
             # reason should always be not None for a started response
             assert self.reason is not None
             self.release()


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
@@ -1243,3 +1243,24 @@ def test_response_links_empty(loop: Any, session: Any) -> None:
     )
     response._headers = CIMultiDict()
     assert response.links == {}
+
+
+def test_response_not_closed_after_get_ok(mocker) -> None:
+    response = ClientResponse(
+        "get",
+        URL("http://del-cl-resp.org"),
+        request_info=mock.Mock(),
+        writer=mock.Mock(),
+        continue100=None,
+        timer=TimerNoop(),
+        traces=[],
+        loop=mock.Mock(),
+        session=mock.Mock(),
+    )
+    response.status = 400
+    response.reason = "Bad Request"
+    response._closed = False
+    spy = mocker.spy(response, "raise_for_status")
+    assert not response.ok
+    assert not response.closed
+    assert spy.call_count == 0