Repository: encode/uvicorn

Issue:
Docs: workers = 1 when WEB_CONCURRENCY is not set
Hello, thanks for this great ASGI server 🙂 

I have an application that must use only one worker (it's not thread-safe), and so I wanted to confirm that, by default, uvicorn only uses one worker. But I had to read the source code

https://github.com/encode/uvicorn/blob/afb2d565c8dae859bcef4c76b3c6dc3f3077314d/uvicorn/config.py#L177

...to confirm that when `WEB_CONCURRENCY` is not defined, and `workers` is not specified, 1 is used.

Would you accept a PR adding that information to the docs (there are 4-5 places where this can be added)?

Discussion:

Patch:
diff --git a/docs/deployment.md b/docs/deployment.md
index cec0d8e..8aabe1b 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -45,7 +45,7 @@ Options:
 
   --workers INTEGER               Number of worker processes. Defaults to the
                                   $WEB_CONCURRENCY environment variable if
-                                  available. Not valid with --reload.
+                                  available, or 1. Not valid with --reload.
 
   --loop [auto|asyncio|uvloop]    Event loop implementation.  [default: auto]
   --http [auto|h11|httptools]     HTTP protocol implementation.  [default:
diff --git a/docs/index.md b/docs/index.md
index be1a096..ae587f2 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -115,7 +115,7 @@ Options:
 
   --workers INTEGER               Number of worker processes. Defaults to the
                                   $WEB_CONCURRENCY environment variable if
-                                  available. Not valid with --reload.
+                                  available, or 1. Not valid with --reload.
 
   --loop [auto|asyncio|uvloop]    Event loop implementation.  [default: auto]
   --http [auto|h11|httptools]     HTTP protocol implementation.  [default:
diff --git a/docs/settings.md b/docs/settings.md
index 2a717ab..8b8398f 100644
--- a/docs/settings.md
+++ b/docs/settings.md
@@ -24,7 +24,7 @@ equivalent keyword arguments, eg. `uvicorn.run("example:app", port=5000, reload=
 
 ## Production
 
-* `--workers <int>` - Use multiple worker processes. Defaults to the value of the `$WEB_CONCURRENCY` environment variable.
+* `--workers <int>` - Use multiple worker processes. Defaults to the `$WEB_CONCURRENCY` environment variable if available, or 1.
 
 ## Logging
 
diff --git a/uvicorn/main.py b/uvicorn/main.py
index 6bfc631..d7e866e 100644
--- a/uvicorn/main.py
+++ b/uvicorn/main.py
@@ -90,7 +90,7 @@ def print_version(ctx, param, value):
     default=None,
     type=int,
     help="Number of worker processes. Defaults to the $WEB_CONCURRENCY environment"
-    " variable if available. Not valid with --reload.",
+    " variable if available, or 1. Not valid with --reload.",
 )
 @click.option(
     "--loop",
diff --git a/uvicorn/protocols/http/h11_impl.py b/uvicorn/protocols/http/h11_impl.py
index 5d2bad4..90850da 100644
--- a/uvicorn/protocols/http/h11_impl.py
+++ b/uvicorn/protocols/http/h11_impl.py
@@ -25,6 +25,8 @@ STATUS_PHRASES = {
     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 @@ class RequestResponseCycle:
             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
index 7be0865..6dcb6e0 100644
--- a/uvicorn/protocols/http/httptools_impl.py
+++ b/uvicorn/protocols/http/httptools_impl.py
@@ -30,6 +30,8 @@ STATUS_LINE = {
     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 @@ class RequestResponseCycle:
             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
index 5a5f3fd..a4c5349 100644
--- 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",