Repository: encode/uvicorn

Issue:
uvicorn closes client connection arbitrarily
I encountered a race condition where uvicorn closes the connection at random. The problem seems to be incorrect management of the keep-alive timer in `uvicorn/protocols/http/httptools_impl.py` (HttpToolsProtocol).

Under some conditions, `on_response_complete` might be called *after* data has been received for the next request. When this happens, there might be two `on_response_complete` calls without a `data_received` call inbetween, hence `timeout_keep_alive_task` is overridden with a new timer without canceling the old one. When the leaked timer expires, the connection is killed.

# Minimal test case:

Server side:
```python
import asyncio
           
async def app(scope, receive, send):
    m = await receive()

    if m['type'] == 'lifespan.startup':
        await send({'type': 'lifespan.startup.complete'})
    elif m['type'] == 'http.request':
        await asyncio.sleep(.2)
        await send({'type': 'http.response.start', 'status': 200, 'headers': [(b'content-length',b'5')]})
        await send({'type': 'http.response.body', 'body': b'data\n', 'more_body': True})
        await asyncio.sleep(.5)
        await send({'type': 'http.response.body', 'body': b'', 'more_body': False})
```

Client side:
```python
import requests
import time
s = requests.Session()
for i in range(2):
    s.get(f'http://localhost:8000/{i}')
time.sleep(1)
for i in range(10):
    s.get(f'http://localhost:8000/{i}')
```


Discussion:
@itayperl 

The bug is reproducible using your snippet.

Probably yes, the bug related to the fact, that setting new keep-alive task, we do not cancel the old-one

Resolving patch:
diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py
--- a/uvicorn/protocols/http/h11_impl.py
+++ b/uvicorn/protocols/http/h11_impl.py
@@ -153,11 +153,14 @@ def connection_lost(self, exc):
     def eof_received(self):
         pass
 
-    def data_received(self, data):
+    def _unset_keepalive_if_required(self):
         if self.timeout_keep_alive_task is not None:
             self.timeout_keep_alive_task.cancel()
             self.timeout_keep_alive_task = None
 
+    def data_received(self, data):
+        self._unset_keepalive_if_required()
+
         self.conn.receive_data(data)
         self.handle_events()
 
@@ -299,6 +302,8 @@ def on_response_complete(self):
             return
 
         # Set a short Keep-Alive timeout.
+        self._unset_keepalive_if_required()
+
         self.timeout_keep_alive_task = self.loop.call_later(
             self.timeout_keep_alive, self.timeout_keep_alive_handler
         )
diff --git a/uvicorn/protocols/http/httptools_impl.py b/uvicorn/protocols/http/httptools_impl.py
--- a/uvicorn/protocols/http/httptools_impl.py
+++ b/uvicorn/protocols/http/httptools_impl.py
@@ -153,11 +153,14 @@ def connection_lost(self, exc):
     def eof_received(self):
         pass
 
-    def data_received(self, data):
+    def _unset_keepalive_if_required(self):
         if self.timeout_keep_alive_task is not None:
             self.timeout_keep_alive_task.cancel()
             self.timeout_keep_alive_task = None
 
+    def data_received(self, data):
+        self._unset_keepalive_if_required()
+
         try:
             self.parser.feed_data(data)
         except httptools.HttpParserError:
@@ -301,6 +304,8 @@ def on_response_complete(self):
             return
 
         # Set a short Keep-Alive timeout.
+        self._unset_keepalive_if_required()
+
         self.timeout_keep_alive_task = self.loop.call_later(
             self.timeout_keep_alive, self.timeout_keep_alive_handler
         )


Test patch: