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:
Looks like it's a case when sending a response and the connection has closed prematurely.

Might be resolved by `if self.disconnected: return` at the start of the `send()` method.

Is it easy to reproduce?
I am not sure how to reproduce. In one project it is happening for every 5 requests or so and it is failing randomly. In other projects its not happening at all.
Okay. I’m guessing you’re not seeing error on the client side? Looks like we should issue a warning, and then return in this case. The application is sending data but the connection has been closed by the client, which isn’t really an error case, but is worth logging with a warning. Will see if can figure what Daphne does in this cases.
I think daphne just silently ignores this case. Do you see the same behaviour running with ‘—http h11’?
> Okay. I’m guessing you’re not seeing error on the client side?

No

For h11 also, it is throwing error

```
ERROR: Exception in callback UVTransport._call_connection_lost
handle: <Handle UVTransport._call_connection_lost>
Traceback (most recent call last):
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/h11/_state.py", line 249, in _fire_event_triggered_transitions
    new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type]
KeyError: <class 'h11._events.ConnectionClosed'>

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "uvloop/cbhandles.pyx", line 70, in uvloop.loop.Handle._run
  File "uvloop/handles/basetransport.pyx", line 202, in uvloop.loop.UVBaseTransport._call_connection_lost
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/uvicorn/protocols/http/h11.py", line 79, in connection_lost
    self.conn.send(event)
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/h11/_connection.py", line 464, in send
    data_list = self.send_with_data_passthrough(event)
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/h11/_connection.py", line 490, in send_with_data_passthrough
    self._process_event(self.our_role, event)
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/h11/_connection.py", line 238, in _process_event
    self._cstate.process_event(role, type(event), server_switch_event)
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/h11/_state.py", line 238, in process_event
    self._fire_event_triggered_transitions(role, event_type)
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/h11/_state.py", line 253, in _fire_event_triggered_transitions
    .format(event_type, role, self.states[role]))
h11._util.LocalProtocolError: can't handle event type <class 'h11._events.ConnectionClosed'> for SERVER in state SEND_BODY

ERROR: Exception in ASGI application
Traceback (most recent call last):
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/uvicorn/protocols/http/h11.py", line 171, 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/h11.py", line 261, in send
    output = protocol.conn.send(event)
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/h11/_connection.py", line 464, in send
    data_list = self.send_with_data_passthrough(event)
  File "/home/chillar/.virtualenvs/library/lib/python3.6/site-packages/h11/_connection.py", line 480, in send_with_data_passthrough
    "Can't send data when our state is ERROR")
h11._util.LocalProtocolError: Can't send data when our state is ERROR
```

Resolving patch:
diff --git a/uvicorn/__init__.py b/uvicorn/__init__.py
--- 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
--- a/uvicorn/protocols/http/h11.py
+++ b/uvicorn/protocols/http/h11.py
@@ -72,11 +72,15 @@ def connection_lost(self, exc):
         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 @@ async def send(self, message):
         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
--- a/uvicorn/protocols/http/httptools.py
+++ b/uvicorn/protocols/http/httptools.py
@@ -83,7 +83,7 @@ def connection_lost(self, exc):
         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 @@ async def send(self, message):
         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
--- a/tests/protocols/test_http.py
+++ b/tests/protocols/test_http.py
@@ -340,6 +340,17 @@ async def __call__(self, receive, send):
     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):