From 9aeeab4341cbe06e8071561de9a4fd62ed9d1372 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Mon, 29 Jun 2026 09:30:45 +0200 Subject: [PATCH 1/2] fix: normalize unicode url_prefix to match PEP 3333 path encoding --- src/waitress/adjustments.py | 5 +++++ tests/test_adjustments.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/waitress/adjustments.py b/src/waitress/adjustments.py index 8b26eb87..9389c4ed 100644 --- a/src/waitress/adjustments.py +++ b/src/waitress/adjustments.py @@ -75,6 +75,11 @@ def slash_fixed_str(s): # always have a leading slash, replace any number of leading slashes # with a single slash, and strip any trailing slashes s = "/" + s.lstrip("/").rstrip("/") + # Normalize encoding to match PEP 3333: WSGI PATH_INFO/SCRIPT_NAME + # are raw URL bytes decoded as ISO-8859-1. Encode the user-supplied + # Unicode string as UTF-8 and decode as ISO-8859-1 so that non-ASCII + # characters compare equal to the parser-produced path. + s = s.encode("utf-8").decode("iso-8859-1") return s diff --git a/tests/test_adjustments.py b/tests/test_adjustments.py index 86bf5ded..591b3ac4 100644 --- a/tests/test_adjustments.py +++ b/tests/test_adjustments.py @@ -384,6 +384,27 @@ def test_server_header_removable(self): inst = self._makeOne(ident="specific_header") self.assertEqual(inst.ident, "specific_header") + def test_url_prefix_unicode_normalization(self): + from waitress.parser import unquote_bytes_to_wsgi + + # Non-ASCII url_prefix should be normalized to PEP 3333 format + # (UTF-8-encoded bytes decoded as ISO-8859-1) so it compares + # equal to the parser-produced path for the same logical path. + inst = self._makeOne(url_prefix="/\xfc/") # ü as latin-1 byte + # \xfc (ü in latin-1) encoded as UTF-8 is \xc3\xbc. + # Decoded as latin-1 that gives two code points: \xc3 \xbc. + # Note: slash_fixed_str strips any trailing slash. + self.assertEqual(inst.url_prefix, "/ü") + + # ASCII-only url_prefix must still be unchanged. + inst2 = self._makeOne(url_prefix="/api") + self.assertEqual(inst2.url_prefix, "/api") + + # Now check it matches a parser-produced path for /ü + raw_uri = b"/%C3%BC" + path = unquote_bytes_to_wsgi(raw_uri) + self.assertEqual(inst.url_prefix, path) + class TestCLI(unittest.TestCase): def parse(self, argv): From 1bd6c93fc5a1efa88e0d2fdb17e479f0f1f963cc Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Fri, 31 Jul 2026 22:00:13 +0200 Subject: [PATCH 2/2] docs: note the url_prefix normalization in CHANGES --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index c7f32ea4..a1b2dae3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,11 @@ Unreleased Bugfix ~~~~~~ +- Waitress now normalizes a non-ASCII ``url_prefix`` to the PEP 3333 + encoding used for request paths, so the prefix matches the parsed + ``PATH_INFO``/``SCRIPT_NAME``. See + https://github.com/Pylons/waitress/pull/493. + - Renamed the HTTP header "Trailers" to "Trailer" to fix a typo and comply with the correct header name as specified in RFC 7230.