Repository: encode/uvicorn

Issue:
Likely race condition with keep-alive http handling
If you run this ASGI application,
```python
import asyncio                                                                                                                                                                                              
                                                                                                                                                                                                            
                                                                                                                                                                                                            
class App:                                                                                                                                                                                                  
                                                                                                                                                                                                            
    def __init__(self, scope):                                                                                                                                                                              
        pass                                                                                                                                                                                                
                                                                                                                                                                                                            
    async def __call__(self, receive, send):                                                                                                                                                                
        body = bytearray()                                                                                                                                                                                  
        while True:                                                                                                                                                                                         
            await asyncio.sleep(0)  # This line is sufficient to break Uvicorn serving                                                                                                                      
            message = await receive()                                                                                                                                                                       
            if message['type'] == 'http.disconnect':                                                                                                                                                        
                return                                                                                                                                                                                      
            elif message['type'] == 'http.request':                                                                                                                                                         
                body.extend(message.get('body', b''))                                                                                                                                                       
                if not message.get('more_body', False):                                                                                                                                                     
                    await send({                                                                                                                                                                            
                        'type': 'http.response.start',                                                                                                                                                      
                        'status': 200,                                                                                                                                                                      
                        'headers': [(b'content-length', b"%d" % len(body))],                                                                                                                                
                    })                                                                                                                                                                                      
                    await send({                                                                                                                                                                            
                        'type': 'http.response.body',                                                                                                                                                       
                        'body': bytes(body),                                                                                                                                                                
                        'more_body': False,                                                                                                                                                                 
                    })                                                                                                                                                                                      
            elif message['type'] == 'lifespan.startup':                                                                                                                                                     
                await send({'type': 'lifespan.startup.complete'})                                                                                                                                           
            elif message['type'] == 'lifespan.shutdown':                                                                                                                                                    
                await send({'type': 'lifespan.shutdown.complete'})
```
with and without the `asyncio.sleep` line with a client that utilises a keep-alive connection you can hit a state (with the line) whereby the serving hangs. This requires a client that sends the next request as soon as it receives the last line of the previous, e.g. `wrk`. 

I think what happens is that the previous request ASGI instance receives the disconnect message after the server has received the next request body. The next request ASGI app then hangs waiting for a `http.request` message from Uvicorn as the previous request consumed the `event_message` message.


Discussion:
I forgot to emphasize that this is very hard to see without a body, so I use this script with `wrk`,
```                                                                                                                             
wrk.method = "POST"
wrk.body   = "fib=10"
wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"
```
Thanks @pgjones, much appreciated!

Are you able to replicate this at debug level? It'd help if we could unpick the very last few ASGI messages that get sent. Also does it replicate with both `--http h11` and `--http httptools`?
I think this is the relevant part from `wrk -d 10s -s scripts/post.lua http://localhost:8000`,
```
DEBUG: ('127.0.0.1', 52733) - ASGI [12] Initialized {'type': 'http', 'http_version': '1.1', 'server': ('127.0.0.1', 8000), 'client': ('127.0.0.1', 52733), 'scheme': 'http', 'method': 'POST', 'root_path': '', 'path': '/', 'query_string': b'', 'headers': '<...>'}
DEBUG: ('127.0.0.1', 52733) - ASGI [12] Started task
DEBUG: ('127.0.0.1', 52733) - ASGI [3] Sent {'type': 'http.disconnect'}
DEBUG: ('127.0.0.1', 52733) - ASGI [3] Completed
```
I can replicate in both h11 and httptools. 
Hey guys! 

@pgjones your snippet looks similar to #250. In my case I trigger issue by sending a multipart file upload payload with cURL and it doesn't like what it gets back. 

Is there a connection? 🤔 
Simpler replication:

**script**

```
wrk.method = "POST"
wrk.body   = "fib=10"
wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"
```

**running wrk**

`wrk -d 10s -t 1 -c 1 -s tmp http://localhost:8000`

**app**...

```python
class App:

    def __init__(self, scope):
        assert scope['type'] == 'http'

    async def __call__(self, receive, send):
        await asyncio.sleep(0)  # This line is sufficient to break Uvicorn serving
        message = await receive()

        body = b'hello'
        await send({
            'type': 'http.response.start',
            'status': 200,
            'headers': [(b'content-length', b"%d" % len(body))],
        })
        await send({
            'type': 'http.response.body',
            'body': body,
            'more_body': False,
        })
        message = await receive()  # Wait for the disconnect
```

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
@@ -507,9 +507,10 @@ async def receive(self):
             self.transport.write(output)
             self.waiting_for_100_continue = False
 
-        self.flow.resume_reading()
-        await self.message_event.wait()
-        self.message_event.clear()
+        if not self.disconnected and not self.response_complete:
+            self.flow.resume_reading()
+            await self.message_event.wait()
+            self.message_event.clear()
 
         if self.disconnected or self.response_complete:
             message = {"type": "http.disconnect"}
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
@@ -517,9 +517,10 @@ async def receive(self):
             self.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n")
             self.waiting_for_100_continue = False
 
-        self.flow.resume_reading()
-        await self.message_event.wait()
-        self.message_event.clear()
+        if not self.disconnected and not self.response_complete:
+            self.flow.resume_reading()
+            await self.message_event.wait()
+            self.message_event.clear()
 
         if self.disconnected or self.response_complete:
             message = {"type": "http.disconnect"}


Test patch: