From bbde5c09490ad981fa5a3d1701b959e7f111e2fd Mon Sep 17 00:00:00 2001 From: agu2347 Date: Tue, 28 Jul 2026 10:01:39 +0000 Subject: [PATCH] Fix non-ASCII url_prefix never matching the request path HTTPTask.execute() compares the configured url_prefix directly against the request path. Per PEP 3333, WSGI represents PATH_INFO/SCRIPT_NAME as "native strings" holding the raw request bytes decoded as latin-1 (each byte becomes the code point of the same value), regardless of the request's actual encoding. The configured url_prefix, however, was left as the ordinary Unicode string the user wrote it as, with no equivalent conversion applied. For an ASCII-only url_prefix this happens to work, since UTF-8-encoding pure ASCII text and decoding it back as latin-1 is a no-op. For a non-ASCII url_prefix, the two representations are entirely different strings (verified: a 3-character Japanese prefix produces two visually and byte-wise unrelated strings), so the comparison in execute() could never succeed, making it impossible to mount an application behind any non-ASCII url_prefix at all. Apply the same "UTF-8 bytes decoded as latin-1" conversion to url_prefix at config-parsing time (in the slash_fixed_str converter, the only place it's used), matching the exact fix suggested in the issue. This only affects non-ASCII url_prefix values; ASCII values, the empty string, and the existing slash-normalization behavior are completely unaffected, since the added encode/decode round-trip is a no-op for pure ASCII input. Verified directly: constructed an Adjustments instance with a non-ASCII url_prefix and confirmed it now exactly matches the latin-1-decoded UTF-8 bytes representation that HTTPTask.execute() would compare it against for an incoming request to that path -- the exact mismatch described in the issue is resolved. Also confirmed ASCII url_prefix values, the empty string, and slash-normalization (e.g. "///foo/" -> "/foo") are unaffected. Added regression tests: one confirming a non-ASCII url_prefix is correctly converted to match the WSGI path-encoding convention, and one confirming ASCII url_prefix values are unaffected. Confirmed the non-ASCII test fails with the original code (produces two unrelated strings) and passes with the fix. Ran the full existing test_adjustments.py and test_task.py suites: 124 passed (122 baseline + 2 new); the 3 remaining failures (socket-binding tests) are pre-existing sandbox limitations, confirmed identical on a clean checkout of main. Fixes #492 --- CHANGES.txt | 7 +++++++ src/waitress/adjustments.py | 11 +++++++++++ tests/test_adjustments.py | 23 +++++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index c7f32ea4..5966bf8c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,13 @@ Unreleased Bugfix ~~~~~~ +- A non-ASCII ``url_prefix`` configuration value is now converted to the same + "UTF-8 bytes decoded as latin-1" representation WSGI's PATH_INFO/SCRIPT_NAME + use for the actual request path, so it can now correctly match incoming + requests. Previously, a non-ASCII url_prefix could never match any request + path at all. See + https://github.com/Pylons/waitress/issues/492 + - Renamed the HTTP header "Trailers" to "Trailer" to fix a typo and comply with the correct header name as specified in RFC 7230. diff --git a/src/waitress/adjustments.py b/src/waitress/adjustments.py index 8b26eb87..65a84a2b 100644 --- a/src/waitress/adjustments.py +++ b/src/waitress/adjustments.py @@ -75,6 +75,17 @@ 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("/") + # Per PEP 3333, WSGI's PATH_INFO/SCRIPT_NAME are represented as + # "native strings" holding raw request bytes decoded as latin-1 + # (i.e. each byte becomes the code point of the same value), + # regardless of the request's actual encoding. The configured + # url_prefix, in contrast, is an ordinary Unicode string as the + # user wrote it. Encoding it the same way here (UTF-8 bytes, + # then decoded as latin-1) ensures comparisons against the + # request path (see HTTPTask.execute()) use the same + # convention on both sides, so a non-ASCII url_prefix can + # actually match. See GH #492. + s = s.encode("utf-8").decode("latin-1") return s diff --git a/tests/test_adjustments.py b/tests/test_adjustments.py index 86bf5ded..379c7953 100644 --- a/tests/test_adjustments.py +++ b/tests/test_adjustments.py @@ -174,6 +174,29 @@ def test_goodvars(self): # localhost... self.assertTupleEqual(("127.0.0.1", 8080), bind_pairs[0]) + def test_url_prefix_non_ascii_matches_wsgi_encoded_path(self): + """ + Regression test for + https://github.com/Pylons/waitress/issues/492 + + A non-ASCII url_prefix must be converted to the same + "utf8-as-latin1" representation WSGI's PATH_INFO/SCRIPT_NAME + use for the actual request path (see HTTPTask.execute() in + task.py), or a non-ASCII url_prefix can never match an + incoming request's path. + """ + inst = self._makeOne(url_prefix="/日本語") + + # What the request path would actually look like once the raw + # UTF-8 bytes sent by a client are decoded as latin-1, per + # PEP 3333's WSGI string convention. + expected = "/日本語".encode("utf-8").decode("latin-1") + self.assertEqual(inst.url_prefix, expected) + + def test_url_prefix_ascii_is_unaffected(self): + inst = self._makeOne(url_prefix="///api/v1///") + self.assertEqual(inst.url_prefix, "/api/v1") + def test_goodvar_listen(self): inst = self._makeOne(listen="127.0.0.1")