Repository: encode/uvicorn

Issue:
Allow specifying reason during websocket close
### Checklist

<!-- Please make sure you check all these items before submitting your feature request. -->

- [x] There are no similar issues or pull requests for this yet.
- [x] I discussed this idea on the [community chat](https://gitter.im/encode/community) and feedback is positive.

### Is your feature related to a problem? Please describe.

At the moment applications can't specify the reason (see https://tools.ietf.org/html/rfc6455#section-5.5.1) when closing a websocket, just the code.

## Describe the solution you would like.

After https://github.com/django/asgiref/pull/235 ASGI allows applications to specify reason, so let's add support for that.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b5f5ca..2328484 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,20 @@
 # Change Log
 
+## 0.13.4 - 2021-02-20
+
+### Fixed
+
+- Fixed wsgi middleware PATH_INFO encoding (#962) 2/20/21
+- Fixed uvloop dependency  (#952) 2/10/21 then (#959) 2/20/21
+- Relax watchgod up bound (#946) 1/31/21
+- Return 'connection: close' header in response (#721) 1/25/21
+
+### Added:
+
+- Docs: Nginx + websockets (#948) 2/10/21 
+- Document the default value of 1 for workers (#940) (#943) 1/25/21
+- Enabled permessage-deflate extension in websockets (#764) 1/1/21
+
 ## 0.13.3 - 2020-12-29
 
 ### Fixed
diff --git a/setup.py b/setup.py
index 5f41722..73f1771 100755
--- a/setup.py
+++ b/setup.py
@@ -53,8 +53,7 @@ minimal_requirements = [
 extra_requirements = [
     "websockets==8.*",
     "httptools==0.1.* ;" + env_marker_cpython,
-    "uvloop==0.14.0; python_version == '3.6' and " + env_marker_cpython,
-    "uvloop>=0.14.0; python_version >= '3.7' and " + env_marker_cpython,
+    "uvloop>=0.14.0,!=0.15.0,!=0.15.1; " + env_marker_cpython,
     "colorama>=0.4;" + env_marker_win,
     "watchgod>=0.6",
     "python-dotenv>=0.13",
diff --git a/uvicorn/__init__.py b/uvicorn/__init__.py
index 9392a50..63b79ff 100644
--- a/uvicorn/__init__.py
+++ b/uvicorn/__init__.py
@@ -1,5 +1,5 @@
 from uvicorn.config import Config
 from uvicorn.main import Server, main, run
 
-__version__ = "0.13.3"
+__version__ = "0.13.4"
 __all__ = ["main", "run", "Config", "Server"]
diff --git a/uvicorn/middleware/wsgi.py b/uvicorn/middleware/wsgi.py
index e5346dd..fca747e 100644
--- a/uvicorn/middleware/wsgi.py
+++ b/uvicorn/middleware/wsgi.py
@@ -11,7 +11,7 @@ def build_environ(scope, message, body):
     environ = {
         "REQUEST_METHOD": scope["method"],
         "SCRIPT_NAME": "",
-        "PATH_INFO": scope["path"],
+        "PATH_INFO": scope["path"].encode("utf8").decode("latin1"),
         "QUERY_STRING": scope["query_string"].decode("ascii"),
         "SERVER_PROTOCOL": "HTTP/%s" % scope["http_version"],
         "wsgi.version": (1, 0),
diff --git a/uvicorn/protocols/websockets/websockets_impl.py b/uvicorn/protocols/websockets/websockets_impl.py
index 6057fec..4afd72f 100644
--- a/uvicorn/protocols/websockets/websockets_impl.py
+++ b/uvicorn/protocols/websockets/websockets_impl.py
@@ -224,7 +224,8 @@ class WebSocketProtocol(websockets.WebSocketServerProtocol):
 
             elif message_type == "websocket.close":
                 code = message.get("code", 1000)
-                await self.close(code)
+                reason = message.get("reason", "")
+                await self.close(code, reason)
                 self.closed_event.set()
 
             else:
diff --git a/uvicorn/protocols/websockets/wsproto_impl.py b/uvicorn/protocols/websockets/wsproto_impl.py
index 456af13..6d15f83 100644
--- a/uvicorn/protocols/websockets/wsproto_impl.py
+++ b/uvicorn/protocols/websockets/wsproto_impl.py
@@ -279,8 +279,11 @@ class WSProtocol(asyncio.Protocol):
             elif message_type == "websocket.close":
                 self.close_sent = True
                 code = message.get("code", 1000)
+                reason = message.get("reason", "")
                 self.queue.put_nowait({"type": "websocket.disconnect", "code": code})
-                output = self.conn.send(wsproto.events.CloseConnection(code=code))
+                output = self.conn.send(
+                    wsproto.events.CloseConnection(code=code, reason=reason)
+                )
                 if not self.transport.is_closing():
                     self.transport.write(output)
                     self.transport.close()


Test patch:
diff --git a/tests/middleware/test_wsgi.py b/tests/middleware/test_wsgi.py
index dd4c311..b2ea797 100644
--- a/tests/middleware/test_wsgi.py
+++ b/tests/middleware/test_wsgi.py
@@ -3,7 +3,7 @@ import sys
 import httpx
 import pytest
 
-from uvicorn.middleware.wsgi import WSGIMiddleware
+from uvicorn.middleware.wsgi import WSGIMiddleware, build_environ
 
 
 def hello_world(environ, start_response):
@@ -94,3 +94,17 @@ async def test_wsgi_exc_info():
         response = await client.get("/")
     assert response.status_code == 500
     assert response.text == "Internal Server Error"
+
+
+def test_build_environ_encoding():
+    scope = {
+        "type": "http",
+        "http_version": "1.1",
+        "method": "GET",
+        "path": "/文",
+        "root_path": "/文",
+        "query_string": b"a=123&b=456",
+        "headers": [],
+    }
+    environ = build_environ(scope, b"", b"")
+    assert environ["PATH_INFO"] == "/文".encode("utf8").decode("latin-1")
diff --git a/tests/protocols/test_websocket.py b/tests/protocols/test_websocket.py
index 57a8c7a..00e0969 100644
--- a/tests/protocols/test_websocket.py
+++ b/tests/protocols/test_websocket.py
@@ -377,14 +377,24 @@ async def test_asgi_return_value(protocol_cls):
 
 @pytest.mark.asyncio
 @pytest.mark.parametrize("protocol_cls", WS_PROTOCOLS)
-async def test_app_close(protocol_cls):
+@pytest.mark.parametrize("code", [None, 1000, 1001])
+@pytest.mark.parametrize("reason", [None, "test"])
+async def test_app_close(protocol_cls, code, reason):
     async def app(scope, receive, send):
         while True:
             message = await receive()
             if message["type"] == "websocket.connect":
                 await send({"type": "websocket.accept"})
             elif message["type"] == "websocket.receive":
-                await send({"type": "websocket.close"})
+                reply = {"type": "websocket.close"}
+
+                if code is not None:
+                    reply["code"] = code
+
+                if reason is not None:
+                    reply["reason"] = reason
+
+                await send(reply)
             elif message["type"] == "websocket.disconnect":
                 break
 
@@ -398,7 +408,8 @@ async def test_app_close(protocol_cls):
     async with run_server(config):
         with pytest.raises(websockets.exceptions.ConnectionClosed) as exc_info:
             await websocket_session("ws://127.0.0.1:8000")
-        assert exc_info.value.code == 1000
+        assert exc_info.value.code == (code or 1000)
+        assert exc_info.value.reason == (reason or "")
 
 
 @pytest.mark.asyncio