from ._content_streams import ContentStream
from ._exceptions import (
HTTPCORE_EXC_MAP,
+ InvalidURL,
+ RemoteProtocolError,
RequestBodyUnavailable,
TooManyRedirects,
map_exceptions,
"""
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
+ TooManyRedirects
+ RequestBodyUnavailable
x HTTPStatusError
+* InvalidURL
* NotRedirectResponse
* CookieConflict
* StreamError
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.
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]],
import chardet
import rfc3986
+import rfc3986.exceptions
from .__version__ import __version__
from ._content_streams import ByteStream, ContentStream, encode
from ._exceptions import (
CookieConflict,
HTTPStatusError,
+ InvalidURL,
NotRedirectResponse,
RequestNotRead,
ResponseClosed,
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.
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":
"""
AsyncClient,
Client,
NotRedirectResponse,
+ RemoteProtocolError,
RequestBodyUnavailable,
TooManyRedirects,
UnsupportedProtocol,
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/")]
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())
import pytest
-from httpx import URL
+from httpx import URL, InvalidURL
@pytest.mark.parametrize(
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://😇/")
def test_get_invalid_url():
- with pytest.raises(httpx.InvalidURL):
+ with pytest.raises(httpx.UnsupportedProtocol):
httpx.get("invalid://example.org")