]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Map rfc3986 exceptions (#1163)
authorJoe <nigelchiang@outlook.com>
Tue, 11 Aug 2020 08:44:56 +0000 (16:44 +0800)
committerGitHub <noreply@github.com>
Tue, 11 Aug 2020 08:44:56 +0000 (09:44 +0100)
* Map rfc3896 exceptions

httpx/_client.py
httpx/_exceptions.py
httpx/_models.py
tests/client/test_redirects.py
tests/models/test_url.py
tests/test_api.py

index a9fd495c91082796a33cf86dfa31bcd40a6f268c..1211790757298b1a4c3f4113445bb850865b1239 100644 (file)
@@ -19,6 +19,8 @@ from ._config import (
 from ._content_streams import ContentStream
 from ._exceptions import (
     HTTPCORE_EXC_MAP,
+    InvalidURL,
+    RemoteProtocolError,
     RequestBodyUnavailable,
     TooManyRedirects,
     map_exceptions,
@@ -340,7 +342,12 @@ class BaseClient:
         """
         location = response.headers["Location"]
 
-        url = URL(location)
+        try:
+            url = URL(location)
+        except InvalidURL as exc:
+            raise RemoteProtocolError(
+                f"Invalid URL in location header: {exc}.", request=request
+            ) from None
 
         # Handle malformed 'Location' headers that are "absolute" form, have no host.
         # See: https://github.com/encode/httpx/issues/771
index 436e4709d5b597aebd1dca809edcd88a433b9cf7..9a46d7d24a7054915acbad4e06824cb188284894 100644 (file)
@@ -23,6 +23,7 @@ Our exception hierarchy:
     + TooManyRedirects
     + RequestBodyUnavailable
   x HTTPStatusError
+* InvalidURL
 * NotRedirectResponse
 * CookieConflict
 * StreamError
@@ -230,6 +231,15 @@ class HTTPStatusError(HTTPError):
         self.response = response
 
 
+class InvalidURL(Exception):
+    """
+    URL is improperly formed or cannot be parsed.
+    """
+
+    def __init__(self, message: str) -> None:
+        super().__init__(message)
+
+
 class NotRedirectResponse(Exception):
     """
     Response was not a redirect response.
@@ -323,14 +333,6 @@ class ResponseClosed(StreamError):
         super().__init__(message)
 
 
-# The `InvalidURL` class is no longer required. It was being used to enforce only
-# 'http'/'https' URLs being requested, but is now treated instead at the
-# transport layer using `UnsupportedProtocol()`.`
-
-# We are currently still exposing this class, but it will be removed in 1.0.
-InvalidURL = UnsupportedProtocol
-
-
 @contextlib.contextmanager
 def map_exceptions(
     mapping: typing.Mapping[typing.Type[Exception], typing.Type[Exception]],
index 17c6ef6ee20c05ef84dd752fc86615b76d82c2e5..a5913c43c476d46988880b49845542aec0981e56 100644 (file)
@@ -11,6 +11,7 @@ from urllib.parse import parse_qsl, urlencode
 
 import chardet
 import rfc3986
+import rfc3986.exceptions
 
 from .__version__ import __version__
 from ._content_streams import ByteStream, ContentStream, encode
@@ -25,6 +26,7 @@ from ._decoders import (
 from ._exceptions import (
     CookieConflict,
     HTTPStatusError,
+    InvalidURL,
     NotRedirectResponse,
     RequestNotRead,
     ResponseClosed,
@@ -57,7 +59,11 @@ from ._utils import (
 class URL:
     def __init__(self, url: URLTypes = "", params: QueryParamTypes = None) -> None:
         if isinstance(url, str):
-            self._uri_reference = rfc3986.iri_reference(url).encode()
+            try:
+                self._uri_reference = rfc3986.iri_reference(url).encode()
+            except rfc3986.exceptions.InvalidAuthority as exc:
+                raise InvalidURL(message=str(exc)) from None
+
             if self.is_absolute_url:
                 # We don't want to normalize relative URLs, since doing so
                 # removes any leading `../` portion.
@@ -183,7 +189,7 @@ class URL:
 
             kwargs["authority"] = authority
 
-        return URL(self._uri_reference.copy_with(**kwargs).unsplit(),)
+        return URL(self._uri_reference.copy_with(**kwargs).unsplit())
 
     def join(self, url: URLTypes) -> "URL":
         """
index e699bb6945c73b2e5fe11ebbddd197a8c98ff943..7c3cbd716ced0c70027981143753dad1efe305f9 100644 (file)
@@ -10,6 +10,7 @@ from httpx import (
     AsyncClient,
     Client,
     NotRedirectResponse,
+    RemoteProtocolError,
     RequestBodyUnavailable,
     TooManyRedirects,
     UnsupportedProtocol,
@@ -75,6 +76,11 @@ class MockTransport:
             headers = [(b"location", b"https://:443/")]
             return b"HTTP/1.1", status_code, b"See Other", headers, ByteStream(b"")
 
+        elif path == b"/invalid_redirect":
+            status_code = codes.SEE_OTHER
+            headers = [(b"location", "https://😇/".encode("utf-8"))]
+            return b"HTTP/1.1", status_code, b"See Other", headers, ByteStream(b"")
+
         elif path == b"/no_scheme_redirect":
             status_code = codes.SEE_OTHER
             headers = [(b"location", b"//example.org/")]
@@ -249,6 +255,13 @@ async def test_malformed_redirect():
     assert len(response.history) == 1
 
 
+@pytest.mark.usefixtures("async_environment")
+async def test_invalid_redirect():
+    client = AsyncClient(transport=AsyncMockTransport())
+    with pytest.raises(RemoteProtocolError):
+        await client.get("http://example.org/invalid_redirect")
+
+
 @pytest.mark.usefixtures("async_environment")
 async def test_no_scheme_redirect():
     client = AsyncClient(transport=AsyncMockTransport())
index 6c0fdac82b01c70c45f656336fc21fb1ff19c668..19e07575d39172551cf341f8830c043b5625f327 100644 (file)
@@ -1,6 +1,6 @@
 import pytest
 
-from httpx import URL
+from httpx import URL, InvalidURL
 
 
 @pytest.mark.parametrize(
@@ -185,3 +185,8 @@ def test_url_copywith_for_authority():
     for k, v in copy_with_kwargs.items():
         assert getattr(new, k) == v
     assert str(new) == "https://username:password@example.net:444"
+
+
+def test_url_invalid():
+    with pytest.raises(InvalidURL):
+        URL("https://😇/")
index 2d51d99e8a4860fc2bde85f5fe6fc7b7d4addd19..2332a9db3d11c5666e0b73884253f0a4568355de 100644 (file)
@@ -69,5 +69,5 @@ def test_stream(server):
 
 
 def test_get_invalid_url():
-    with pytest.raises(httpx.InvalidURL):
+    with pytest.raises(httpx.UnsupportedProtocol):
         httpx.get("invalid://example.org")