Skip to content

Commit

Permalink
Use '%20' for encoding spaces in query parameters. (#2543)
Browse files Browse the repository at this point in the history
* Add failing test

* Fix failing test case

* Add urlencode

* Update comment
  • Loading branch information
tomchristie committed Jan 10, 2023
1 parent 57daabf commit a6af45e
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 2 deletions.
16 changes: 16 additions & 0 deletions httpx/_urlparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,3 +433,19 @@ def quote(string: str, safe: str = "/") -> str:
return "".join(
[char if char in NON_ESCAPED_CHARS else percent_encode(char) for char in string]
)


def urlencode(items: typing.List[typing.Tuple[str, str]]) -> str:
# We can use a much simpler version of the stdlib urlencode here because
# we don't need to handle a bunch of different typing cases, such as bytes vs str.
#
# https://github.com/python/cpython/blob/b2f7b2ef0b5421e01efb8c7bee2ef95d3bab77eb/Lib/urllib/parse.py#L926
#
# Note that we use '%20' encoding for spaces, and treat '/' as a safe
# character. This means our query params have the same escaping as other
# characters in the URL path. This is slightly different to `requests`,
# but is the behaviour that browsers use.
#
# See https://github.com/encode/httpx/issues/2536 and
# https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlencode
return "&".join([quote(k) + "=" + quote(v) for k, v in items])
11 changes: 9 additions & 2 deletions httpx/_urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import typing
from urllib.parse import parse_qs, unquote, urlencode
from urllib.parse import parse_qs, unquote

import idna

from ._types import PrimitiveData, QueryParamTypes, RawURL, URLTypes
from ._urlparse import urlparse
from ._urlparse import urlencode, urlparse
from ._utils import primitive_value_to_str


Expand Down Expand Up @@ -616,6 +616,13 @@ def __eq__(self, other: typing.Any) -> bool:
return sorted(self.multi_items()) == sorted(other.multi_items())

def __str__(self) -> str:
"""
Note that we use '%20' encoding for spaces, and treat '/' as a safe
character.
See https://github.com/encode/httpx/issues/2536 and
https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlencode
"""
return urlencode(self.multi_items())

def __repr__(self) -> str:
Expand Down
16 changes: 16 additions & 0 deletions tests/models/test_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,22 @@ def test_url_with_empty_query():
assert url.raw_path == b"/path?"


def test_url_query_encoding():
"""
URL query parameters should use '%20' to encoding spaces,
and should treat '/' as a safe character. This behaviour differs
across clients, but we're matching browser behaviour here.
See https://github.com/encode/httpx/issues/2536
and https://github.com/encode/httpx/discussions/2460
"""
url = httpx.URL("https://www.example.com/?a=b c&d=e/f")
assert url.raw_path == b"/?a=b%20c&d=e/f"

url = httpx.URL("https://www.example.com/", params={"a": "b c", "d": "e/f"})
assert url.raw_path == b"/?a=b%20c&d=e/f"


def test_url_with_url_encoded_path():
url = httpx.URL("https://www.example.com/path%20to%20somewhere")
assert url.path == "/path to somewhere"
Expand Down

0 comments on commit a6af45e

Please sign in to comment.