diff --git a/CHANGES/2983.feature b/CHANGES/2983.feature new file mode 100644 index 00000000000..5e5b60abe1d --- /dev/null +++ b/CHANGES/2983.feature @@ -0,0 +1 @@ +Forbid reading response BODY after release \ No newline at end of file diff --git a/aiohttp/client.py b/aiohttp/client.py index 2e5d4ee38cd..45f2737e33a 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -362,8 +362,6 @@ async def _request(self, method, url, *, resp.close() raise TooManyRedirects( history[0].request_info, tuple(history)) - else: - resp.release() # For 301 and 302, mimic IE, now changed in RFC # https://github.com/kennethreitz/requests/pull/269 @@ -381,6 +379,10 @@ async def _request(self, method, url, *, if r_url is None: # see github.com/aio-libs/aiohttp/issues/2022 break + else: + # reading from correct redirection + # response is forbidden + resp.release() try: r_url = URL( diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index f4688cc300e..070c2cfdf1f 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -577,6 +577,7 @@ class ClientResponse(HeadersMixin): # setted up by ClientRequest after ClientResponse object creation # post-init stage allows to not change ctor signature _closed = True # to allow __del__ for non-initialized properly response + _released = False def __init__(self, method, url, *, writer, continue100, timer, @@ -795,6 +796,8 @@ def closed(self): return self._closed def close(self): + if not self._released: + self._notify_content() if self._closed: return @@ -806,9 +809,10 @@ def close(self): self._connection.close() self._connection = None self._cleanup_writer() - self._notify_content() def release(self): + if not self._released: + self._notify_content() if self._closed: return noop() @@ -818,7 +822,6 @@ def release(self): self._connection = None self._cleanup_writer() - self._notify_content() return noop() def raise_for_status(self): @@ -838,9 +841,10 @@ def _cleanup_writer(self): def _notify_content(self): content = self.content - if content and content.exception() is None and not content.is_eof(): + if content and content.exception() is None: content.set_exception( ClientConnectionError('Connection closed')) + self._released = True async def wait_for_close(self): if self._writer is not None: @@ -860,6 +864,8 @@ async def read(self): except BaseException: self.close() raise + elif self._released: + raise ClientConnectionError('Connection closed') return self._body diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 4e5f89e4602..7fc8af3c430 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -2549,3 +2549,52 @@ async def gen(): resp = await client.post('/', data=gen()) assert resp.status == 200 + + +async def test_read_from_closed_response(aiohttp_client): + async def handler(request): + return web.Response(body=b'data') + + app = web.Application() + app.add_routes([web.get('/', handler)]) + + client = await aiohttp_client(app) + + async with client.get('/') as resp: + assert resp.status == 200 + + with pytest.raises(aiohttp.ClientConnectionError): + await resp.read() + + +async def test_read_from_closed_response2(aiohttp_client): + async def handler(request): + return web.Response(body=b'data') + + app = web.Application() + app.add_routes([web.get('/', handler)]) + + client = await aiohttp_client(app) + + async with client.get('/') as resp: + assert resp.status == 200 + await resp.read() + + with pytest.raises(aiohttp.ClientConnectionError): + await resp.read() + + +async def test_read_from_closed_content(aiohttp_client): + async def handler(request): + return web.Response(body=b'data') + + app = web.Application() + app.add_routes([web.get('/', handler)]) + + client = await aiohttp_client(app) + + async with client.get('/') as resp: + assert resp.status == 200 + + with pytest.raises(aiohttp.ClientConnectionError): + await resp.content.readline()