Repository: aio-libs/aiohttp

Issue:
SSL with closed connections
## Long story short

Receiving strange errors when bots are connecting to my SSL (HTTPS) server without sending any data and disconnecting afterwards.
SSL protocol handler inside aiohttp shows unexpected errors. This never happens to plain HTTP connections.

## Expected behaviour

No messages received (as for HTTP protocol).

## Actual behaviour

Multiple errors:
task: <Task pending coro=<RequestHandler.start() done, defined at C:\Program Files\Python36\lib\site-packages\aiohttp\web_protocol.py:342> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x000001E0FBF59D98>()]>>
Task was destroyed but it is pending!

## Steps to reproduce

Run simple example:

```
import asyncio  
import aiohttp.web
import ssl

async def fetch_page():
    while True:
        reader, writer = await asyncio.open_connection('127.0.0.1', 443)
        writer.close()
    
async def request_handler(request: aiohttp.web.BaseRequest):
    await request.read()
    return aiohttp.web.Response(status=200, content_type='text/html')

async def start_server():
    ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    ssl_context.check_hostname = False
    ssl_context.load_cert_chain('any.crt', 'any.key')

    server = aiohttp.web.Server(request_handler)
    await loop.create_server(server, '127.0.0.1', 443, ssl=ssl_context) # removing ssl=ssl_context disables SSL and fixes the bug
    await fetch_page()

if __name__ == '__main__':  
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.gather(asyncio.Task(start_server())))
```


## Your environment

Checked both Windows 10 and CentOS 7, aiohttp 3.0.4 and 3.3.0


Discussion:
Do you have additional logs? `PYTHONASYNCIODEBUG` mode can help.
`wait_for=...` surprises me, aiohttp doesn't use this function at all.
The following output can be seen in debugging mode:
```C:\>reproduce.py
Executing <Task pending coro=<start_server() running at C:\reproduce.py:31> wait_for=<_GatheringFuture pending cb=[<Task
WakeupMethWrapper object at 0x000001CB1FBC7B28>()] created at C:\Program Files\Python36\lib\asyncio\tasks.py:549> cb=[ga
ther.<locals>._done_callback(0)() at C:\Program Files\Python36\lib\asyncio\tasks.py:616] created at C:\reproduce.py:36>
took 0.172 seconds
Error on transport creation for incoming connection
handle_traceback: Handle created at (most recent call last):
  File "C:\reproduce.py", line 36, in <module>
    loop.run_until_complete(asyncio.gather(asyncio.Task(start_server())))
  File "C:\Program Files\Python36\lib\asyncio\base_events.py", line 455, in run_until_complete
    self.run_forever()
  File "C:\Program Files\Python36\lib\asyncio\base_events.py", line 422, in run_forever
    self._run_once()
  File "C:\Program Files\Python36\lib\asyncio\base_events.py", line 1424, in _run_once
    handle._run()
  File "C:\Program Files\Python36\lib\asyncio\events.py", line 145, in _run
    self._callback(*self._args)
  File "C:\Program Files\Python36\lib\asyncio\selector_events.py", line 734, in _read_ready
    keep_open = self._protocol.eof_received()
  File "C:\Program Files\Python36\lib\asyncio\sslproto.py", line 535, in eof_received
    self._wakeup_waiter(ConnectionResetError)
  File "C:\Program Files\Python36\lib\asyncio\sslproto.py", line 453, in _wakeup_waiter
    self._waiter.set_exception(exc)
protocol: <RequestHandler disconnected>
transport: <asyncio.sslproto._SSLProtocolTransport object at 0x000001CB1FC4E2E8>
Traceback (most recent call last):
  File "C:\Program Files\Python36\lib\asyncio\selector_events.py", line 230, in _accept_connection2
    yield from waiter
ConnectionResetError
```
Thank you. I can reproduce your problem.

Resolving patch:
diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py
--- a/aiohttp/web_protocol.py
+++ b/aiohttp/web_protocol.py
@@ -109,7 +109,7 @@ def __init__(self, manager, *, loop=None,
 
         self._waiter = None
         self._error_handler = None
-        self._task_handler = self._loop.create_task(self.start())
+        self._task_handler = None
 
         self._upgrade = False
         self._payload_parser = None
@@ -158,14 +158,16 @@ async def shutdown(self, timeout=15.0):
         # wait for handlers
         with suppress(asyncio.CancelledError, asyncio.TimeoutError):
             with CeilTimeout(timeout, loop=self._loop):
-                if self._error_handler and not self._error_handler.done():
+                if (self._error_handler is not None and
+                        not self._error_handler.done()):
                     await self._error_handler
 
-                if self._task_handler and not self._task_handler.done():
+                if (self._task_handler is not None and
+                        not self._task_handler.done()):
                     await self._task_handler
 
         # force-close non-idle handler
-        if self._task_handler:
+        if self._task_handler is not None:
             self._task_handler.cancel()
 
         if self.transport is not None:
@@ -180,6 +182,7 @@ def connection_made(self, transport):
 
         tcp_cork(transport, False)
         tcp_nodelay(transport, True)
+        self._task_handler = self._loop.create_task(self.start())
         self._manager.connection_made(self, transport)
 
     def connection_lost(self, exc):
@@ -196,7 +199,7 @@ def connection_lost(self, exc):
         if self._keepalive_handle is not None:
             self._keepalive_handle.cancel()
 
-        if self._task_handler:
+        if self._task_handler is not None:
             self._task_handler.cancel()
 
         if self._error_handler is not None:


Test patch:
diff --git a/tests/test_web_protocol.py b/tests/test_web_protocol.py
--- a/tests/test_web_protocol.py
+++ b/tests/test_web_protocol.py
@@ -647,6 +647,7 @@ def test_rudimentary_transport(srv, loop):
 async def test_close(srv, loop, transport):
     transport.close.side_effect = partial(srv.connection_lost, None)
     srv.connection_made(transport)
+    await asyncio.sleep(0, loop=loop)
 
     srv.handle_request = mock.Mock()
     srv.handle_request.side_effect = helpers.noop