]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Fix Host header and HSTS when the default port is explicitly included in URL (#649)
authorTom Christie <tom@tomchristie.com>
Fri, 20 Dec 2019 10:25:42 +0000 (10:25 +0000)
committerGitHub <noreply@github.com>
Fri, 20 Dec 2019 10:25:42 +0000 (10:25 +0000)
httpx/client.py
httpx/models.py
tests/client/test_headers.py
tests/models/test_url.py

index a6c22d8f49a800409981d3d843602f3778912621..1ceea145d2c72fc8d676b12820bf0aef5c04d767 100644 (file)
@@ -338,7 +338,8 @@ class Client:
         """
         url = self.base_url.join(relative_url=url)
         if url.scheme == "http" and hstspreload.in_hsts_preload(url.host):
-            url = url.copy_with(scheme="https")
+            port = None if url.port == 80 else url.port
+            url = url.copy_with(scheme="https", port=port)
         return url
 
     def merge_cookies(
index ba19f43fe2455b68a15e34e5db81d5af7561b9be..ad3599b717433099ffdb50d36cc13a9eb061a6de 100644 (file)
@@ -112,6 +112,10 @@ class URL:
 
     @property
     def authority(self) -> str:
+        port_str = self._uri_reference.port
+        default_port_str = {"https": "443", "http": "80"}.get(self.scheme, "")
+        if port_str is None or port_str == default_port_str:
+            return self._uri_reference.host or ""
         return self._uri_reference.authority or ""
 
     @property
@@ -203,9 +207,9 @@ class URL:
             authority = host
             if port is not None:
                 authority += f":{port}"
-            if username is not None:
+            if username:
                 userpass = username
-                if password is not None:
+                if password:
                     userpass += f":{password}"
                 authority = f"{userpass}@{authority}"
 
index 37a5683d7f61e8d4cd52b9193de4343d0b451d39..168b6a04df2c88e7bb39a2076f85855666758510 100755 (executable)
@@ -130,7 +130,12 @@ def test_header_does_not_exist():
 
 
 @pytest.mark.asyncio
-async def test_host_without_auth_in_header():
+async def test_host_with_auth_and_port_in_url():
+    """
+    The Host header should only include the hostname, or hostname:port
+    (for non-default ports only). Any userinfo or default port should not
+    be present.
+    """
     url = "http://username:password@example.org:80/echo_headers"
 
     client = Client(dispatch=MockDispatch())
@@ -142,7 +147,31 @@ async def test_host_without_auth_in_header():
             "accept": "*/*",
             "accept-encoding": "gzip, deflate, br",
             "connection": "keep-alive",
-            "host": "example.org:80",
+            "host": "example.org",
+            "user-agent": f"python-httpx/{__version__}",
+            "authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
+        }
+    }
+
+
+@pytest.mark.asyncio
+async def test_host_with_non_default_port_in_url():
+    """
+    If the URL includes a non-default port, then it should be included in
+    the Host header.
+    """
+    url = "http://username:password@example.org:123/echo_headers"
+
+    client = Client(dispatch=MockDispatch())
+    response = await client.get(url)
+
+    assert response.status_code == 200
+    assert response.json() == {
+        "headers": {
+            "accept": "*/*",
+            "accept-encoding": "gzip, deflate, br",
+            "connection": "keep-alive",
+            "host": "example.org:123",
             "user-agent": f"python-httpx/{__version__}",
             "authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
         }
index f6c7ab4de7f36e1b6bcca305fb57f78b1fac1a9f..1415ee9143a2113d7e64058c14c4a32162af17a4 100644 (file)
@@ -73,8 +73,8 @@ def test_url():
         repr(url) == "URL('https://example.org:123/path/to/somewhere?abc=123#anchor')"
     )
 
-    new = url.copy_with(scheme="http")
-    assert new == URL("http://example.org:123/path/to/somewhere?abc=123#anchor")
+    new = url.copy_with(scheme="http", port=None)
+    assert new == URL("http://example.org/path/to/somewhere?abc=123#anchor")
     assert new.scheme == "http"