diff --git a/src/wsgi/callbacks.rs b/src/wsgi/callbacks.rs index 7654c504..76445a26 100644 --- a/src/wsgi/callbacks.rs +++ b/src/wsgi/callbacks.rs @@ -38,12 +38,13 @@ fn build_wsgi( protocol: WSGIProtocol, body: WSGIBody, ) -> PyResult<(Py, Bound)> { - let (path, query_string) = req.uri.path_and_query().map_or_else( - || (String::new(), String::new()), + let (path, query_string, raw_uri) = req.uri.path_and_query().map_or_else( + || (String::new(), String::new(), String::new()), |pq| { ( encoding_rs::mem::decode_latin1(&percent_decode_str(pq.path()).collect_vec()).into_owned(), encoding_rs::mem::decode_latin1(pq.query().unwrap_or("").as_bytes()).into_owned(), + encoding_rs::mem::decode_latin1(pq.as_str().as_bytes()).into_owned(), ) }, ); @@ -68,6 +69,9 @@ fn build_wsgi( environ_set!(py, environ, "REQUEST_METHOD", req.method.as_str()); environ_set!(py, environ, "PATH_INFO", path); environ_set!(py, environ, "QUERY_STRING", query_string); + // Not in PEP 3333, but gunicorn sets it and middleware (e.g. OpenTelemetry) reads it + // to recover the target as sent, since PATH_INFO is percent-decoded. + environ_set!(py, environ, "RAW_URI", raw_uri); environ_set!(py, environ, "wsgi.url_scheme", scheme.as_str()); environ_set!(py, environ, "wsgi.input", body); diff --git a/tests/apps/wsgi.py b/tests/apps/wsgi.py index b103a0b2..b293a92d 100644 --- a/tests/apps/wsgi.py +++ b/tests/apps/wsgi.py @@ -10,6 +10,7 @@ def info(environ, protocol): 'method': environ['REQUEST_METHOD'], 'path': environ['PATH_INFO'], 'query_string': environ['QUERY_STRING'], + 'raw_uri': environ['RAW_URI'], 'content_length': environ.get('CONTENT_LENGTH'), 'headers': {k: v for k, v in environ.items() if k.startswith('HTTP_')}, } @@ -36,6 +37,11 @@ def err_app(environ, protocol): def app(environ, protocol): - return {'/info': info, '/echo': echo, '/iterbody': iterbody, '/err_app': err_app}[environ['PATH_INFO']]( - environ, protocol - ) + return { + '/info': info, + # PATH_INFO for /info/%E6%B5%8B%2F: percent-decoded, then decoded as latin-1 + '/info/' + '测'.encode().decode('latin-1') + '/': info, + '/echo': echo, + '/iterbody': iterbody, + '/err_app': err_app, + }[environ['PATH_INFO']](environ, protocol) diff --git a/tests/test_wsgi.py b/tests/test_wsgi.py index 1fd27fb2..db6df3db 100644 --- a/tests/test_wsgi.py +++ b/tests/test_wsgi.py @@ -22,11 +22,24 @@ async def test_scope(wsgi_server, runtime_mode): assert data['method'] == 'POST' assert data['path'] == '/info' assert data['query_string'] == 'test=true' + assert data['raw_uri'] == '/info?test=true' assert data['headers']['HTTP_HOST'] == f'localhost:{port}' assert data['content_length'] == str(len(payload)) assert data['headers']['HTTP_TEST'] == 'val1,val2' +@pytest.mark.asyncio +@pytest.mark.parametrize('runtime_mode', ['mt', 'st']) +async def test_raw_uri(wsgi_server, runtime_mode): + async with wsgi_server(runtime_mode) as port: + res = httpx.get(f'http://localhost:{port}/info/%E6%B5%8B%2F?q=%E6%B5%8B') + + assert res.status_code == 200 + data = res.json() + assert data['path'] == '/info/' + '测'.encode().decode('latin-1') + '/' + assert data['raw_uri'] == '/info/%E6%B5%8B%2F?q=%E6%B5%8B' + + @pytest.mark.asyncio @pytest.mark.parametrize('runtime_mode', ['mt', 'st']) async def test_body(wsgi_server, runtime_mode):