Repository: aio-libs/aiohttp

Issue:
3.3 socket read timeouts not working correctly
@asvetlov looks like the new `sock_read` timeout implementation is not working, see testcase:

```python
import multiprocessing
import asyncio
import aiohttp
import socket

host = "localhost"


def get_free_tcp_port():
    sckt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sckt.bind((host, 0))
    addr, port = sckt.getsockname()
    sckt.close()
    return port


class AIOServer(multiprocessing.Process):
    """
    This is a mock AWS service which will 5 seconds before returning
    a response to test socket timeouts.
    """
    def __init__(self):
        super().__init__(target=self._run)
        self._loop = None
        self._port = get_free_tcp_port()
        self.endpoint_url = 'http://{}:{}'.format(host, self._port)
        self.daemon = True  # die when parent dies

    def _run(self):
        asyncio.set_event_loop(asyncio.new_event_loop())
        app = aiohttp.web.Application()
        app.router.add_route('*', '/ok', self.ok)
        app.router.add_route('*', '/{anything:.*}', self.stream_handler)

        try:
            aiohttp.web.run_app(app, host=host, port=self._port,
                                handle_signals=False)
        except BaseException:
            pytest.fail('unable to start and connect to aiohttp server')
            raise

    async def __aenter__(self):
        self.start()
        await self._wait_until_up()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        try:
            self.terminate()
        except:
            pytest.fail("Unable to shut down server")
            raise

    async def ok(self, request):
        return aiohttp.web.Response()

    async def stream_handler(self, request):
        # Without the Content-Type, most (all?) browsers will not render
        # partially downloaded content. Note, the response type is
        # StreamResponse not Response.
        resp = aiohttp.web.StreamResponse(status=200, reason='OK',
                              headers={'Content-Type': 'text/html'})

        await resp.prepare(request)
        await asyncio.sleep(5, loop=self._loop)
        await resp.drain()
        return resp

    async def _wait_until_up(self):
        async with aiohttp.ClientSession() as session:
            for i in range(0, 30):
                if self.exitcode is not None:
                    pytest.fail('unable to start and connect to aiohttp server')
                    return

                try:
                    # we need to bypass the proxies due to monkey patches
                    await session.get(self.endpoint_url + '/ok', timeout=0.5)
                    return
                except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
                    await asyncio.sleep(0.5)
                except BaseException:
                    pytest.fail('unable to start and connect to aiohttp server')
                    raise

        pytest.fail('unable to start and connect to aiohttp server')


async def main():
    async with AIOServer() as server, \
            aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(sock_connect=1, sock_read=1)) as session:

        try:
            response = await session.get(server.endpoint_url)
            await response.read()
        except asyncio.TimeoutError:
            print("Success")

        print("failure")


if __name__ == '__main__':
    asyncio.get_event_loop().run_until_complete(main())
```


Discussion:

Resolving patch:
diff --git a/aiohttp/client_proto.py b/aiohttp/client_proto.py
--- a/aiohttp/client_proto.py
+++ b/aiohttp/client_proto.py
@@ -51,6 +51,7 @@ def close(self):
             transport.close()
             self.transport = None
             self._payload = None
+            self._drop_timeout()
         return transport
 
     def is_connected(self):
@@ -162,8 +163,10 @@ def _reschedule_timeout(self):
             self._read_timeout_handle = None
 
     def _on_read_timeout(self):
-        self.set_exception(
-            ServerTimeoutError("Timeout on reading data from socket"))
+        exc = ServerTimeoutError("Timeout on reading data from socket")
+        self.set_exception(exc)
+        if self._payload is not None:
+            self._payload.set_exception(exc)
 
     def data_received(self, data):
         if not data:


Test patch:
diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py
--- a/tests/test_client_functional.py
+++ b/tests/test_client_functional.py
@@ -2613,3 +2613,22 @@ async def handler(request):
 
     with pytest.raises(aiohttp.ServerTimeoutError):
         await client.get('/')
+
+
+async def test_read_timeout_on_prepared_response(aiohttp_client):
+    async def handler(request):
+        resp = aiohttp.web.StreamResponse()
+        await resp.prepare(request)
+        await asyncio.sleep(5)
+        await resp.drain()
+        return resp
+
+    app = web.Application()
+    app.add_routes([web.get('/', handler)])
+
+    timeout = aiohttp.ClientTimeout(sock_read=0.1)
+    client = await aiohttp_client(app, timeout=timeout)
+
+    with pytest.raises(aiohttp.ServerTimeoutError):
+        async with await client.get('/') as resp:
+            await resp.read()