Skip to content

Commit a7092af

Browse files
Resolve queryparam quoting (#3187)
1 parent be56b74 commit a7092af

File tree

2 files changed

+55
-32
lines changed

2 files changed

+55
-32
lines changed

httpx/_urlparse.py

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -406,44 +406,22 @@ def normalize_path(path: str) -> str:
406406
return "/".join(output)
407407

408408

409-
def percent_encode(char: str) -> str:
410-
"""
411-
Replace a single character with the percent-encoded representation.
412-
413-
Characters outside the ASCII range are represented with their a percent-encoded
414-
representation of their UTF-8 byte sequence.
415-
416-
For example:
417-
418-
percent_encode(" ") == "%20"
419-
"""
420-
return "".join([f"%{byte:02x}" for byte in char.encode("utf-8")]).upper()
421-
422-
423-
def is_safe(string: str, safe: str = "/") -> bool:
424-
"""
425-
Determine if a given string is already quote-safe.
426-
"""
427-
NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe + "%"
428-
429-
# All characters must already be non-escaping or '%'
430-
for char in string:
431-
if char not in NON_ESCAPED_CHARS:
432-
return False
433-
434-
return True
409+
def PERCENT(string: str) -> str:
410+
return "".join([f"%{byte:02X}" for byte in string.encode("utf-8")])
435411

436412

437413
def percent_encoded(string: str, safe: str = "/") -> str:
438414
"""
439415
Use percent-encoding to quote a string.
440416
"""
441-
if is_safe(string, safe=safe):
417+
NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe
418+
419+
# Fast path for strings that don't need escaping.
420+
if not string.rstrip(NON_ESCAPED_CHARS):
442421
return string
443422

444-
NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe
445423
return "".join(
446-
[char if char in NON_ESCAPED_CHARS else percent_encode(char) for char in string]
424+
[char if char in NON_ESCAPED_CHARS else PERCENT(char) for char in string]
447425
)
448426

449427

tests/models/test_url.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,11 @@ def test_url_normalized_host():
229229
assert url.host == "example.com"
230230

231231

232+
def test_url_percent_escape_host():
233+
url = httpx.URL("https://exam%le.com/")
234+
assert url.host == "exam%25le.com"
235+
236+
232237
def test_url_ipv4_like_host():
233238
"""rare host names used to quality as IPv4"""
234239
url = httpx.URL("https://023b76x43144/")
@@ -278,24 +283,64 @@ def test_url_leading_dot_prefix_on_relative_url():
278283
assert url.path == "../abc"
279284

280285

281-
# Tests for optional percent encoding
286+
# Tests for query parameter percent encoding.
287+
#
288+
# Percent-encoding in `params={}` should match browser form behavior.
282289

283290

284-
def test_param_requires_encoding():
291+
def test_param_with_space():
292+
# Params passed as form key-value pairs should be escaped.
285293
url = httpx.URL("http://webservice", params={"u": "with spaces"})
286294
assert str(url) == "http://webservice?u=with%20spaces"
287295

288296

289297
def test_param_does_not_require_encoding():
298+
# Params passed as form key-value pairs should be escaped.
299+
url = httpx.URL("http://webservice", params={"u": "%"})
300+
assert str(url) == "http://webservice?u=%25"
301+
302+
303+
def test_param_with_percent_encoded():
304+
# Params passed as form key-value pairs should always be escaped,
305+
# even if they include a valid escape sequence.
306+
# We want to match browser form behaviour here.
290307
url = httpx.URL("http://webservice", params={"u": "with%20spaces"})
291-
assert str(url) == "http://webservice?u=with%20spaces"
308+
assert str(url) == "http://webservice?u=with%2520spaces"
292309

293310

294311
def test_param_with_existing_escape_requires_encoding():
312+
# Params passed as form key-value pairs should always be escaped,
313+
# even if they include a valid escape sequence.
314+
# We want to match browser form behaviour here.
295315
url = httpx.URL("http://webservice", params={"u": "http://example.com?q=foo%2Fa"})
296316
assert str(url) == "http://webservice?u=http%3A%2F%2Fexample.com%3Fq%3Dfoo%252Fa"
297317

298318

319+
# Tests for query parameter percent encoding.
320+
#
321+
# Percent-encoding in `url={}` should match browser URL bar behavior.
322+
323+
324+
def test_query_with_existing_percent_encoding():
325+
# Valid percent encoded sequences should not be double encoded.
326+
url = httpx.URL("http://webservice?u=phrase%20with%20spaces")
327+
assert str(url) == "http://webservice?u=phrase%20with%20spaces"
328+
329+
330+
def test_query_requiring_percent_encoding():
331+
# Characters that require percent encoding should be encoded.
332+
url = httpx.URL("http://webservice?u=phrase with spaces")
333+
assert str(url) == "http://webservice?u=phrase%20with%20spaces"
334+
335+
336+
def test_query_with_mixed_percent_encoding():
337+
# When a mix of encoded and unencoded characters are present,
338+
# characters that require percent encoding should be encoded,
339+
# while existing sequences should not be double encoded.
340+
url = httpx.URL("http://webservice?u=phrase%20with spaces")
341+
assert str(url) == "http://webservice?u=phrase%20with%20spaces"
342+
343+
299344
# Tests for invalid URLs
300345

301346

0 commit comments

Comments
 (0)