Repository: encode/uvicorn

Issue:
WSGI middleware error in non-latin1 path
### Checklist

<!-- Please make sure you check all these items before submitting your bug report. -->

- [x] The bug is reproducible against the latest release and/or `master`.
- [x] There are no similar issues or pull requests to fix it yet.

### Describe the bug

<!-- A clear and concise description of what the bug is. -->

The same bug has been addressed in https://github.com/encode/starlette/issues/997.

https://github.com/encode/uvicorn/blob/9d51e1cfa68ee6e4ee3fb8a23b8599bb44c3985a/uvicorn/middleware/wsgi.py#L14

In uvicorn's wsgi middleware, `scope["path"]` in asgi is utf8-encoded but the corresponding `PATH_INFO` required by wsgi should be encoded in latin1 according to the standards.

### To reproduce

<!-- Provide a *minimal* example with steps to reproduce the bug locally.

NOTE: try to keep any external dependencies *at an absolute minimum* .
In other words, remove anything that doesn't make the bug go away.

-->

```python
import flask
import uvicorn

app = flask.Flask(__name__)

@app.route("/<keyword>")
def home(keyword):
    return keyword

if __name__ == "__main__":
    uvicorn.run(app, interface="wsgi")
```

1. run the script.
2. send a request to "http://127.0.0.1:8000/文"

### Expected behavior

<!-- A clear and concise description of what you expected to happen. -->

Get a 200 response

### Actual behavior

<!-- A clear and concise description of what actually happens. -->

Get 500 Internal Server Error

### Debugging material

<!-- Any tracebacks, screenshots, etc. that can help understanding the problem.

NOTE:
- Please list tracebacks in full (don't truncate them).
- If relevant, consider turning on DEBUG or TRACE logs for additional details (see the Logging section on https://www.uvicorn.org/settings/ specifically the `log-level` flag).
- Consider using `<details>` to make tracebacks/logs collapsible if they're very large (see https://gist.github.com/ericclemmons/b146fe5da72ca1f706b2ef72a20ac39d).
-->

<details>

```python
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "C:\Users\cools\AppData\Local\Programs\Python\Python38\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 394, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "C:\Users\cools\AppData\Local\Programs\Python\Python38\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 45, in __call__
    return await self.app(scope, receive, send)
  File "C:\Users\cools\AppData\Local\Programs\Python\Python38\lib\site-packages\uvicorn\middleware\wsgi.py", line 64, in __call__
    await instance(receive, send)
  File "C:\Users\cools\AppData\Local\Programs\Python\Python38\lib\site-packages\uvicorn\middleware\wsgi.py", line 95, in __call__
    await asyncio.wait_for(wsgi, None)
  File "C:\Users\cools\AppData\Local\Programs\Python\Python38\lib\asyncio\tasks.py", line 455, in wait_for
    return await fut
  File "C:\Users\cools\AppData\Local\Programs\Python\Python38\lib\concurrent\futures\thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "C:\Users\cools\AppData\Local\Programs\Python\Python38\lib\site-packages\uvicorn\middleware\wsgi.py", line 134, in wsgi
    for chunk in self.app(environ, start_response):
  File "C:\Users\cools\AppData\Local\Programs\Python\Python38\lib\site-packages\flask\app.py", line 2464, in __call__
    return self.wsgi_app(environ, start_response)
  File "C:\Users\cools\AppData\Local\Programs\Python\Python38\lib\site-packages\flask\app.py", line 2442, in wsgi_app
    ctx = self.request_context(environ)
  File "C:\Users\cools\AppData\Local\Programs\Python\Python38\lib\site-packages\flask\app.py", line 2359, in request_context
    return RequestContext(self, environ)
  File "C:\Users\cools\AppData\Local\Programs\Python\Python38\lib\site-packages\flask\ctx.py", line 292, in __init__
    self.url_adapter = app.create_url_adapter(self.request)
  File "C:\Users\cools\AppData\Local\Programs\Python\Python38\lib\site-packages\flask\app.py", line 2171, in create_url_adapter
    return self.url_map.bind_to_environ(
  File "C:\Users\cools\AppData\Local\Programs\Python\Python38\lib\site-packages\werkzeug\routing.py", line 1634, in bind_to_environ
    path_info = _get_wsgi_string("PATH_INFO")
  File "C:\Users\cools\AppData\Local\Programs\Python\Python38\lib\site-packages\werkzeug\routing.py", line 1631, in _get_wsgi_string
    return wsgi_decoding_dance(val, self.charset)
  File "C:\Users\cools\AppData\Local\Programs\Python\Python38\lib\site-packages\werkzeug\_compat.py", line 188, in wsgi_decoding_dance
    return s.encode("latin1").decode(charset, errors)
UnicodeEncodeError: 'latin-1' codec can't encode character '\u6587' in position 1: ordinal not in range(256)
INFO:     127.0.0.1:52574 - "GET /%E6%96%87 HTTP/1.1" 500 Internal Server Error
```

</details>

### Environment

- OS / Python / Uvicorn version: Running uvicorn 0.13.3 with CPython 3.8.5 on Windows
- The exact command you're running uvicorn with, all flags you passed included. If you run it with gunicorn please do the same. If there is a reverse-proxy involved and you cannot reproduce without it please give the minimal config of it to reproduce.

### Additional context

<!-- Any additional information that can help understanding the problem.

Eg. linked issues, or a description of what you were trying to achieve. -->

Discussion:
CoolSpring8: I can see that SCRIPT_NAME is tracked down in #937, so I leave it untouched for now.
(though the original fix in https://github.com/encode/starlette/pull/998 indicates both PATH_INFO and SCRIPT_NAME should be changed)

Patch:
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),


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")