Repository: encode/uvicorn

Issue:
RuntimeError in uvicorn for some requests
`uvicorn==0.2.5` is throwing errors for some requests.

```
ERROR: Exception in ASGI application
Traceback (most recent call last):
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/uvicorn/protocols/http/httptools.py", line 196, in run_asgi
    result = await asgi(self.receive, self.send)
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/channels/http.py", line 190, in __call__
    await self.handle(body)
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/asgiref/sync.py", line 110, in __call__
    return await asyncio.wait_for(future, timeout=None)
  File "/usr/lib/python3.6/asyncio/tasks.py", line 339, in wait_for
    return (yield from fut)
  File "/usr/lib/python3.6/concurrent/futures/thread.py", line 56, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/asgiref/sync.py", line 125, in thread_handler
    return self.func(*args, **kwargs)
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/channels/http.py", line 229, in handle
    self.send(response_message)
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/asgiref/sync.py", line 64, in __call__
    return call_result.result()
  File "/usr/lib/python3.6/concurrent/futures/_base.py", line 432, in result
    return self.__get_result()
  File "/usr/lib/python3.6/concurrent/futures/_base.py", line 384, in __get_result
    raise self._exception
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/asgiref/sync.py", line 78, in main_wrap
    result = await self.awaitable(*args, **kwargs)
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/uvicorn/protocols/http/httptools.py", line 308, in send
    protocol.transport.write(body)
  File "uvloop/handles/stream.pyx", line 636, in uvloop.loop.UVStream.write
  File "uvloop/handles/handle.pyx", line 165, in uvloop.loop.UVHandle._ensure_alive
RuntimeError: unable to perform operation on <TCPTransport closed=True reading=False 0x1a48ad8>; the handler is closed
```


Discussion:

Patch:
diff --git a/uvicorn/__init__.py b/uvicorn/__init__.py
index bfd395e..385fbc3 100644
--- a/uvicorn/__init__.py
+++ b/uvicorn/__init__.py
@@ -1,4 +1,4 @@
 from uvicorn.main import main, run
 
-__version__ = "0.2.5"
+__version__ = "0.2.6"
 __all__ = ["main", "run"]
diff --git a/uvicorn/protocols/http/h11.py b/uvicorn/protocols/http/h11.py
index 6848630..073cddd 100644
--- a/uvicorn/protocols/http/h11.py
+++ b/uvicorn/protocols/http/h11.py
@@ -72,11 +72,15 @@ class H11Protocol(asyncio.Protocol):
         if self.access_logs:
             self.logger.debug("%s - Disconnected", self.server[0])
 
-        if self.cycle and self.cycle.more_body:
+        if self.cycle and not self.cycle.response_complete:
             self.cycle.disconnected = True
         if self.conn.our_state != h11.ERROR:
             event = h11.ConnectionClosed()
-            self.conn.send(event)
+            try:
+                self.conn.send(event)
+            except h11.LocalProtocolError:
+                # Premature client disconnect
+                pass
         self.client_event.set()
 
     def eof_received(self):
@@ -215,6 +219,9 @@ class RequestResponseCycle:
         protocol = self.protocol
         message_type = message["type"]
 
+        if self.disconnected:
+            return
+
         if not protocol.writable:
             await protocol.writable_event.wait()
 
diff --git a/uvicorn/protocols/http/httptools.py b/uvicorn/protocols/http/httptools.py
index 7344b01..ebbbb39 100644
--- a/uvicorn/protocols/http/httptools.py
+++ b/uvicorn/protocols/http/httptools.py
@@ -83,7 +83,7 @@ class HttpToolsProtocol(asyncio.Protocol):
         if self.access_logs:
             self.logger.debug("%s - Disconnected", self.server[0])
 
-        if self.cycle and self.cycle.more_body:
+        if self.cycle and not self.cycle.response_complete:
             self.cycle.disconnected = True
         self.client_event.set()
 
@@ -238,6 +238,9 @@ class RequestResponseCycle:
         protocol = self.protocol
         message_type = message["type"]
 
+        if self.disconnected:
+            return
+
         if not protocol.writable:
             await protocol.writable_event.wait()
 


Test patch:
diff --git a/tests/protocols/test_http.py b/tests/protocols/test_http.py
index 88658dd..3b08fee 100644
--- a/tests/protocols/test_http.py
+++ b/tests/protocols/test_http.py
@@ -340,6 +340,17 @@ def test_value_returned(protocol_cls):
     assert protocol.transport.is_closing()
 
 
+@pytest.mark.parametrize("protocol_cls", [HttpToolsProtocol, H11Protocol])
+def test_early_disconnect(protocol_cls):
+    def app(scope):
+        return Response(b"xxx", headers={"content-length": 10})
+
+    protocol = get_connected_protocol(app, protocol_cls)
+    protocol.data_received(SIMPLE_GET_REQUEST)
+    protocol.connection_lost(None)
+    protocol.loop.run_one()
+
+
 @pytest.mark.parametrize("protocol_cls", [HttpToolsProtocol, H11Protocol])
 def test_http10_request(protocol_cls):
     def app(scope):