Repository: encode/httpx

Issue:
Perform HSTS in the Client, not the URL.
I think it'd probably be a good idea for HSTS enforcement to be at the `Client` layer, rather than the `URL` model layer.

Not super keen on instantiating a `URL`, and having it end up with a different scheme, but I *am* okay with `client.request` dealing with it.


Discussion:

Resolving patch:
diff --git a/httpx/client.py b/httpx/client.py
--- a/httpx/client.py
+++ b/httpx/client.py
@@ -2,6 +2,8 @@
 import typing
 from types import TracebackType
 
+import hstspreload
+
 from .auth import HTTPBasicAuth
 from .concurrency import AsyncioBackend
 from .config import (
@@ -102,6 +104,12 @@ def __init__(
         self.dispatch = async_dispatch
         self.concurrency_backend = backend
 
+    def merge_url(self, url: URLTypes) -> URL:
+        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")
+        return url
+
     def merge_cookies(
         self, cookies: CookieTypes = None
     ) -> typing.Optional[CookieTypes]:
@@ -537,7 +545,7 @@ async def request(
         verify: VerifyTypes = None,
         timeout: TimeoutTypes = None,
     ) -> AsyncResponse:
-        url = self.base_url.join(url)
+        url = self.merge_url(url)
         headers = self.merge_headers(headers)
         cookies = self.merge_cookies(cookies)
         request = AsyncRequest(
@@ -619,7 +627,7 @@ def request(
         verify: VerifyTypes = None,
         timeout: TimeoutTypes = None,
     ) -> Response:
-        url = self.base_url.join(url)
+        url = self.merge_url(url)
         headers = self.merge_headers(headers)
         cookies = self.merge_cookies(cookies)
         request = AsyncRequest(
diff --git a/httpx/models.py b/httpx/models.py
--- a/httpx/models.py
+++ b/httpx/models.py
@@ -8,7 +8,6 @@
 from urllib.parse import parse_qsl, urlencode
 
 import chardet
-import hstspreload
 import rfc3986
 
 from .config import USER_AGENT
@@ -109,14 +108,6 @@ def __init__(
             if not self.host:
                 raise InvalidURL("No host included in URL.")
 
-        # If the URL is HTTP but the host is on the HSTS preload list switch to HTTPS.
-        if (
-            self.scheme == "http"
-            and self.host
-            and hstspreload.in_hsts_preload(self.host)
-        ):
-            self._uri_reference = self._uri_reference.copy_with(scheme="https")
-
     @property
     def scheme(self) -> str:
         return self._uri_reference.scheme or ""


Test patch:
diff --git a/tests/client/test_client.py b/tests/client/test_client.py
--- a/tests/client/test_client.py
+++ b/tests/client/test_client.py
@@ -150,3 +150,11 @@ def test_base_url(server):
         response = http.get("/")
     assert response.status_code == 200
     assert str(response.url) == base_url
+
+
+def test_merge_url():
+    client = httpx.Client(base_url="https://www.paypal.com/")
+    url = client.merge_url("http://www.paypal.com")
+
+    assert url.scheme == "https"
+    assert url.is_ssl
diff --git a/tests/models/test_url.py b/tests/models/test_url.py
--- a/tests/models/test_url.py
+++ b/tests/models/test_url.py
@@ -176,11 +176,3 @@ def test_url_set():
     url_set = set(urls)
 
     assert all(url in urls for url in url_set)
-
-
-def test_hsts_preload_converted_to_https():
-    url = URL("http://www.paypal.com")
-
-    assert url.is_ssl
-    assert url.scheme == "https"
-    assert url == "https://www.paypal.com"