Repository: encode/uvicorn

Issue:
connection: close header not returned in response
Hello, the [W3 standard](https://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html) seems to say that a server which receives a request with a `connection: close` header should also return this header to the client. Specifically I'm talking about the first line in `8.1.2.1 Negotiation` quoted here:
> An HTTP/1.1 server MAY assume that a HTTP/1.1 client intends to maintain a persistent connection unless a Connection header including the connection-token "close" was sent in the request. If the server chooses to close the connection immediately after sending the response, it SHOULD send a Connection header including the connection-token close.

The wording is maybe a little ambiguous but to me it definitely seems to imply that if the server closes the connection (wether due to a `connection: close` header in the request or by it's own choice) it should return a `connection: close` header. I ran into an issue with the asks library were this was causing an issue: https://github.com/theelous3/asks/issues/115.


Discussion:

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
@@ -25,6 +25,8 @@ def _get_status_phrase(status_code):
     status_code: _get_status_phrase(status_code) for status_code in range(100, 600)
 }
 
+CLOSE_HEADER = (b"connection", b"close")
+
 HIGH_WATER_LIMIT = 65536
 
 TRACE_LOG_LEVEL = 5
@@ -452,6 +454,9 @@ async def send(self, message):
             status_code = message["status"]
             headers = self.default_headers + message.get("headers", [])
 
+            if CLOSE_HEADER in self.scope["headers"] and CLOSE_HEADER not in headers:
+                headers = headers + [CLOSE_HEADER]
+
             if self.access_log:
                 self.access_logger.info(
                     '%s - "%s %s HTTP/%s" %d',
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
@@ -30,6 +30,8 @@ def _get_status_line(status_code):
     status_code: _get_status_line(status_code) for status_code in range(100, 600)
 }
 
+CLOSE_HEADER = (b"connection", b"close")
+
 HIGH_WATER_LIMIT = 65536
 
 TRACE_LOG_LEVEL = 5
@@ -454,6 +456,9 @@ async def send(self, message):
             status_code = message["status"]
             headers = self.default_headers + list(message.get("headers", []))
 
+            if CLOSE_HEADER in self.scope["headers"] and CLOSE_HEADER not in headers:
+                headers = headers + [CLOSE_HEADER]
+
             if self.access_log:
                 self.access_logger.info(
                     '%s - "%s %s HTTP/%s" %d',


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -11,6 +11,21 @@ async def app(scope, receive, send):
     await send({"type": "http.response.body", "body": b"", "more_body": False})
 
 
+@pytest.mark.asyncio
+async def test_return_close_header():
+    config = Config(app=app, host="localhost", loop="asyncio", limit_max_requests=1)
+    async with run_server(config):
+        async with httpx.AsyncClient() as client:
+            response = await client.get(
+                "http://127.0.0.1:8000", headers={"connection": "close"}
+            )
+
+    assert response.status_code == 204
+    assert (
+        "connection" in response.headers and response.headers["connection"] == "close"
+    )
+
+
 @pytest.mark.asyncio
 @pytest.mark.parametrize(
     "host, url",