Repository: encode/uvicorn

Issue:
ASGI callable returned without completing response & nginx redirect
Hi, I am trying to figure out how to resolve this error and if I am doing something incorrectly. It is triggered when I use the [X-Accel-Redirect](https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/) feature of nginx. This view is working fine as far as I can tell, but after each request I get that exception logged.

My view looks like this:
```
def view(request):
    filename = "some_file"
    path = "/path/to/image.png"
    response = HttpResponse()
    response["Content-Disposition"] = "attachment; filename={}".format(filename)
    response["X-Accel-Redirect"] = "/protected/{}".format(path)
    return response
```

Example log:
```
INFO: / - "GET /app/image/35/ HTTP/1.0" 200
ERROR: ASGI callable returned without completing response.
```


Discussion:
Perhaps try `HttpResponse('')` or `HttpResponse('', status_code=204)`?
I assume that you're returning a response that *looks* as if it has a response body but that then never includes one.

It's not impossible that there's a corner-case bug there for ASGI-over Django that's not been thrashed out there?

I guess you also ought to drop the `Content-Disposition` since I'd assume that nginx would send whatever headers correspond to the media that the internal redirect points at.
Hmm, none of these methods worked. Even including fake content and dropping the content disposition did not help. I will have to continue investigating
Okay. I’m going to close this off for now, since it *looks* like uvicorn is behaving correctly in response to an application issue. What would be helpful tho would be if we had some proper debug-level logging that showed all the ASGI messages, so that this could be debugged more thoroughly.
I have reproduced this with only Django, channels, and uvicorn installed without any custom code. I am still debugging. Do you have any pointers of where to add debug logs to show ASGI messages?

The only thing I am doing to trigger it is refreshing the admin/ page using standard http request. I have been able to reproduce in some situations in both Chrome and Safari, although they are not consistent.
Okay, given that you can reproduce this, and are looking into it, I’m going to reopen, with a view to adding sufficient logging.

There’s a number of different layers where the issue might be - I’d *guess* you’re seeing the issue because uvicorn is (perhaps) stricter than daphne in logging this error case.

I’d suggest logging in `RequestResponseCycle.send/receive`. Alternativly you could write an ASGI middleware to perform the logging.

Presumably what you’ll end up seeing is the start of a 200 OK response, that never gets terminated. (At which point you’d need to walk through channels and/or Django to try to figure out where/why that happens)

Do you have any middleware installed that deals with X-Accel-Redirect responses, or anything like that? Also how is uvicorn routed to the Django app via channels - do HTTP requests get passed directly to the app, or do they go via a channel layer (or whatever that’s called)
Hi Tom, thanks I will start there. I have simplified it to the point where there is no django app, only the admin and default settings.

Here is the code: https://github.com/matteosimone/asgi_error

All I did was install channels, add logging handlers for uvicorn, run migrations, run uvicorn, and visit /admin. At /admin, I refresh the page several times and eventually you should see ASGI error logs.

```
2018-10-03 21:27:37,081.081 - 17050 - 140735659864960 -     INFO - httptools_impl.py:455 - send - 127.0.0.1 - "GET /static/admin/css/base.css HTTP/1.1" 404
Not Found: /static/admin/css/responsive.css
WARNING: Not Found: /static/admin/css/responsive.css
Not Found: /static/admin/css/login.css
WARNING: Not Found: /static/admin/css/login.css
2018-10-03 21:27:37,087.087 - 17050 - 140735659864960 -    DEBUG - httptools_impl.py:154 - connection_lost - 127.0.0.1 - Disconnected
2018-10-03 21:27:37,087.087 - 17050 - 140735659864960 -     INFO - httptools_impl.py:455 - send - 127.0.0.1 - "GET /static/admin/css/responsive.css HTTP/1.1" 404
2018-10-03 21:27:37,088.088 - 17050 - 140735659864960 -     INFO - httptools_impl.py:455 - send - 127.0.0.1 - "GET /static/admin/css/login.css HTTP/1.1" 404
2018-10-03 21:27:37,089.089 - 17050 - 140735659864960 -    ERROR - httptools_impl.py:405 - run_asgi - ASGI callable returned without completing response.
2018-10-03 21:27:42,089.089 - 17050 - 140735659864960 -    DEBUG - httptools_impl.py:154 - connection_lost - 127.0.0.1 - Disconnected
2018-10-03 21:27:42,089.089 - 17050 - 140735659864960 -    DEBUG - httptools_impl.py:154 - connection_lost - 127.0.0.1 - Disconnected
```
I should mention this is the simplest case I found where I see the error. On other views in my project, I am able to see this error logged on every single request.
For the /admin case, I am observing that `connection_lost` is called before the `send` containing `http.response.body`. 

Inside `send` function, you have:
```
        if self.disconnected:
            return
```
so that the `response_complete` flag is never set. I believe this is what triggers the ERROR log. If I modify the source so that it sets the flag, then the error log does disappear in this case. This occurs in Chrome in several instances, most noticeably when a static asset is a 404 or when there is a 302 redirect. I don't know a lot about asgi, but does this change make sense?
Okay, so the HTTP protocols both currently use something like this...

```python
   elif not self.response_started:
        msg = "ASGI callable returned without starting response."
        self.logger.error(msg)
        if not self.disconnected:
            await self.send_500_response()
    elif not self.response_complete:
        msg = "ASGI callable returned without completing response."
        self.logger.error(msg)
        if not self.disconnected:
            self.transport.close()
```

I'd suggest that should instead be...

```python
    elif not self.response_started and not self.disconnected:
        msg = "ASGI callable returned without starting response."
        self.logger.error(msg)
        await self.send_500_response()
    elif not self.response_complete and not self.disconnected:
        msg = "ASGI callable returned without completing response."
        self.logger.error(msg)
        self.transport.close()
```

We'll need that change in both `h11_impl.py` and `httptools_impl.py`
Prompted by this issue: #207
Note that the behavior in #207 is implemented in middleware, so you could add that in to your existing setup if you wanted to take a closer look. (Although I think it looks like we have a resolution to this issue.)

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
@@ -405,16 +405,14 @@ async def run_asgi(self, app):
                 msg = "ASGI callable should return None, but returned '%s'."
                 self.logger.error(msg, result)
                 self.transport.close()
-            elif not self.response_started:
+            elif not self.response_started and not self.disconnected:
                 msg = "ASGI callable returned without starting response."
                 self.logger.error(msg)
-                if not self.disconnected:
-                    await self.send_500_response()
-            elif not self.response_complete:
+                await self.send_500_response()
+            elif not self.response_complete and not self.disconnected:
                 msg = "ASGI callable returned without completing response."
                 self.logger.error(msg)
-                if not self.disconnected:
-                    self.transport.close()
+                self.transport.close()
         finally:
             self.on_response = None
 
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
@@ -395,16 +395,14 @@ async def run_asgi(self, app):
                 msg = "ASGI callable should return None, but returned '%s'."
                 self.logger.error(msg, result)
                 self.transport.close()
-            elif not self.response_started:
+            elif not self.response_started and not self.disconnected:
                 msg = "ASGI callable returned without starting response."
                 self.logger.error(msg)
-                if not self.disconnected:
-                    await self.send_500_response()
-            elif not self.response_complete:
+                await self.send_500_response()
+            elif not self.response_complete and not self.disconnected:
                 msg = "ASGI callable returned without completing response."
                 self.logger.error(msg)
-                if not self.disconnected:
-                    self.transport.close()
+                self.transport.close()
         finally:
             self.on_response = None
 


Test patch: