Skip to content

Commit

Permalink
Fix optional percent encoding behaviour. (#2671)
Browse files Browse the repository at this point in the history
* Tests for failing optional percent encoding

* Linting

* Fix for optional percent escaping
  • Loading branch information
tomchristie committed Apr 19, 2023
1 parent 15d09a3 commit 9ae170a
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 6 deletions.
28 changes: 22 additions & 6 deletions httpx/_urlparse.py
Expand Up @@ -399,7 +399,7 @@ def normalize_path(path: str) -> str:

def percent_encode(char: str) -> str:
"""
Replace every character in a string with the percent-encoded representation.
Replace a single character with the percent-encoded representation.
Characters outside the ASCII range are represented with their a percent-encoded
representation of their UTF-8 byte sequence.
Expand All @@ -411,13 +411,29 @@ def percent_encode(char: str) -> str:
return "".join([f"%{byte:02x}" for byte in char.encode("utf-8")]).upper()


def is_safe(string: str, safe: str = "/") -> bool:
"""
Determine if a given string is already quote-safe.
"""
NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe + "%"

# All characters must already be non-escaping or '%'
for char in string:
if char not in NON_ESCAPED_CHARS:
return False

# Any '%' characters must be valid '%xx' escape sequences.
return string.count("%") == len(PERCENT_ENCODED_REGEX.findall(string))


def quote(string: str, safe: str = "/") -> str:
NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe
if string.count("%") == len(PERCENT_ENCODED_REGEX.findall(string)):
# If all occurances of '%' are valid '%xx' escapes, then treat
# percent as a non-escaping character.
NON_ESCAPED_CHARS += "%"
"""
Use percent-encoding to quote a string if required.
"""
if is_safe(string, safe=safe):
return string

NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe
return "".join(
[char if char in NON_ESCAPED_CHARS else percent_encode(char) for char in string]
)
Expand Down
18 changes: 18 additions & 0 deletions tests/test_urlparse.py
Expand Up @@ -126,6 +126,24 @@ def test_urlparse_leading_dot_prefix_on_relative_url():
assert url.path == "../abc"


# Tests for optional percent encoding


def test_param_requires_encoding():
url = httpx.URL("http://webservice", params={"u": "with spaces"})
assert str(url) == "http://webservice?u=with%20spaces"


def test_param_does_not_require_encoding():
url = httpx.URL("http://webservice", params={"u": "with%20spaces"})
assert str(url) == "http://webservice?u=with%20spaces"


def test_param_with_existing_escape_requires_encoding():
url = httpx.URL("http://webservice", params={"u": "http://example.com?q=foo%2Fa"})
assert str(url) == "http://webservice?u=http%3A//example.com%3Fq%3Dfoo%252Fa"


# Tests for invalid URLs


Expand Down

0 comments on commit 9ae170a

Please sign in to comment.