Skip to content

Commit 167b585

Browse files
Kludexnic-lovin
andauthored
Build request.url from structured components (#3326)
Co-authored-by: nic-lovin <10554285+nic-lovin@users.noreply.github.com>
1 parent 3730925 commit 167b585

2 files changed

Lines changed: 41 additions & 10 deletions

File tree

starlette/datastructures.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,19 @@ def __init__(
4747
break
4848

4949
if host_header is not None and _HOST_RE.fullmatch(host_header):
50-
url = f"{scheme}://{host_header}{path}"
51-
elif server is None:
52-
url = path
53-
else:
50+
netloc = host_header
51+
elif server is not None:
5452
host, port = server
5553
default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme]
56-
if port == default_port:
57-
url = f"{scheme}://{host}{path}"
58-
else:
59-
url = f"{scheme}://{host}:{port}{path}"
54+
netloc = host if port == default_port else f"{host}:{port}"
55+
else:
56+
netloc = None
6057

61-
if query_string:
62-
url += "?" + query_string.decode()
58+
query = query_string.decode()
59+
if netloc is not None:
60+
url = SplitResult(scheme=scheme, netloc=netloc, path=path, query=query, fragment="").geturl()
61+
else:
62+
url = f"{path}?{query}" if query else path
6363
elif components:
6464
assert not url, 'Cannot set both "url" and "**components".'
6565
url = URL("").replace(**components).components.geturl()

tests/test_datastructures.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ def test_url_from_scope() -> None:
126126
assert u == "/path/to/somewhere?abc=123"
127127
assert repr(u) == "URL('/path/to/somewhere?abc=123')"
128128

129+
u = URL(scope={"path": "/path/to/somewhere", "query_string": b"", "headers": []})
130+
assert u == "/path/to/somewhere"
131+
assert repr(u) == "URL('/path/to/somewhere')"
132+
129133
u = URL(
130134
scope={
131135
"scheme": "https",
@@ -192,6 +196,33 @@ def test_url_from_scope_with_invalid_host(host: bytes) -> None:
192196
assert u.netloc == "example.com"
193197

194198

199+
@pytest.mark.parametrize(
200+
"path, expected_path",
201+
[
202+
pytest.param("@google.com", "/@google.com", id="at-sign"),
203+
pytest.param("user:pass@google.com", "/user:pass@google.com", id="userinfo"),
204+
pytest.param("//google.com/x", "//google.com/x", id="scheme-relative"),
205+
pytest.param("http://google.com/x", "/http://google.com/x", id="absolute"),
206+
],
207+
)
208+
@pytest.mark.parametrize("with_host_header", [True, False], ids=["host-header", "server-fallback"])
209+
def test_url_from_scope_with_authority_in_path(path: str, expected_path: str, with_host_header: bool) -> None:
210+
"""A path must not bleed into the authority."""
211+
headers = [(b"host", b"localhost")] if with_host_header else []
212+
u = URL(
213+
scope={
214+
"scheme": "http",
215+
"server": ("localhost", 80),
216+
"path": path,
217+
"query_string": b"a=b",
218+
"headers": headers,
219+
}
220+
)
221+
assert u.hostname == "localhost"
222+
assert u.path == expected_path
223+
assert u.query == "a=b"
224+
225+
195226
def test_headers() -> None:
196227
h = Headers(raw=[(b"a", b"123"), (b"a", b"456"), (b"b", b"789")])
197228
assert "a" in h

0 commit comments

Comments
 (0)