parent
7edfe64da6
commit
45de714592
@ -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
|
||||
|
||||
@ -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]],
|
||||
|
||||
@ -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":
|
||||
"""
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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://😇/")
|
||||
|
||||
@ -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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user