Map rfc3986 exceptions (#1163)

* Map rfc3896 exceptions
This commit is contained in:
Joe 2020-08-11 16:44:56 +08:00 committed by GitHub
parent 7edfe64da6
commit 45de714592
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 46 additions and 13 deletions

View 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

View 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]],

View 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":
"""

View 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())

View 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://😇/")

View 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")