Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
5 changes: 5 additions & 0 deletions src/waitress/adjustments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
21 changes: 21 additions & 0 deletions tests/test_adjustments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down