Skip to content

Commit

Permalink
Exception hierarchy (#1095)
Browse files Browse the repository at this point in the history
* Exception heirachy

* Exception heirarchy

* Formatting tweaks

* Update httpx/_exceptions.py

Co-authored-by: Florimond Manca <[email protected]>

* Update httpx/_exceptions.py

Co-authored-by: Florimond Manca <[email protected]>

* Update httpx/_exceptions.py

Co-authored-by: Florimond Manca <[email protected]>

* Update httpx/_exceptions.py

Co-authored-by: Florimond Manca <[email protected]>

Co-authored-by: Florimond Manca <[email protected]>
  • Loading branch information
tomchristie and florimondmanca authored Jul 31, 2020
1 parent 2ba9c1e commit 9409900
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 108 deletions.
6 changes: 4 additions & 2 deletions httpx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@
ProxyError,
ReadError,
ReadTimeout,
RedirectError,
RequestBodyUnavailable,
RequestError,
RequestNotRead,
ResponseClosed,
ResponseNotRead,
StreamConsumed,
StreamError,
TimeoutException,
TooManyRedirects,
TransportError,
WriteError,
WriteTimeout,
)
Expand Down Expand Up @@ -76,7 +77,7 @@
"ProtocolError",
"ReadError",
"ReadTimeout",
"RedirectError",
"RequestError",
"RequestBodyUnavailable",
"ResponseClosed",
"ResponseNotRead",
Expand All @@ -87,6 +88,7 @@
"ProxyError",
"TimeoutException",
"TooManyRedirects",
"TransportError",
"WriteError",
"WriteTimeout",
"URL",
Expand Down
24 changes: 16 additions & 8 deletions httpx/_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,20 +116,24 @@ def auth_flow(self, request: Request) -> typing.Generator[Request, Response, Non
# need to build an authenticated request.
return

header = response.headers["www-authenticate"]
challenge = self._parse_challenge(header)
challenge = self._parse_challenge(request, response)
request.headers["Authorization"] = self._build_auth_header(request, challenge)
yield request

def _parse_challenge(self, header: str) -> "_DigestAuthChallenge":
def _parse_challenge(
self, request: Request, response: Response
) -> "_DigestAuthChallenge":
"""
Returns a challenge from a Digest WWW-Authenticate header.
These take the form of:
`Digest realm="[email protected]",qop="auth,auth-int",nonce="abc",opaque="xyz"`
"""
header = response.headers["www-authenticate"]

scheme, _, fields = header.partition(" ")
if scheme.lower() != "digest":
raise ProtocolError("Header does not start with 'Digest'")
message = "Header does not start with 'Digest'"
raise ProtocolError(message, request=request)

header_dict: typing.Dict[str, str] = {}
for field in parse_http_list(fields):
Expand All @@ -146,7 +150,8 @@ def _parse_challenge(self, header: str) -> "_DigestAuthChallenge":
realm=realm, nonce=nonce, qop=qop, opaque=opaque, algorithm=algorithm
)
except KeyError as exc:
raise ProtocolError("Malformed Digest WWW-Authenticate header") from exc
message = "Malformed Digest WWW-Authenticate header"
raise ProtocolError(message, request=request) from exc

def _build_auth_header(
self, request: Request, challenge: "_DigestAuthChallenge"
Expand All @@ -171,7 +176,7 @@ def digest(data: bytes) -> bytes:
if challenge.algorithm.lower().endswith("-sess"):
HA1 = digest(b":".join((HA1, challenge.nonce, cnonce)))

qop = self._resolve_qop(challenge.qop)
qop = self._resolve_qop(challenge.qop, request=request)
if qop is None:
digest_data = [HA1, challenge.nonce, HA2]
else:
Expand Down Expand Up @@ -221,7 +226,9 @@ def _get_header_value(self, header_fields: typing.Dict[str, bytes]) -> str:

return header_value

def _resolve_qop(self, qop: typing.Optional[bytes]) -> typing.Optional[bytes]:
def _resolve_qop(
self, qop: typing.Optional[bytes], request: Request
) -> typing.Optional[bytes]:
if qop is None:
return None
qops = re.split(b", ?", qop)
Expand All @@ -231,7 +238,8 @@ def _resolve_qop(self, qop: typing.Optional[bytes]) -> typing.Optional[bytes]:
if qops == [b"auth-int"]:
raise NotImplementedError("Digest auth-int support is not yet implemented")

raise ProtocolError(f'Unexpected qop value "{qop!r}" in digest auth')
message = f'Unexpected qop value "{qop!r}" in digest auth'
raise ProtocolError(message, request=request)


class _DigestAuthChallenge:
Expand Down
20 changes: 12 additions & 8 deletions httpx/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,8 @@ def _redirect_url(self, request: Request, response: Response) -> URL:

# Check that we can handle the scheme
if url.scheme and url.scheme not in ("http", "https"):
raise InvalidURL(f'Scheme "{url.scheme}" not supported.')
message = f'Scheme "{url.scheme}" not supported.'
raise InvalidURL(message, request=request)

# Handle malformed 'Location' headers that are "absolute" form, have no host.
# See: https://github.com/encode/httpx/issues/771
Expand Down Expand Up @@ -537,12 +538,13 @@ def _init_proxy_transport(
http2=http2,
)

def _transport_for_url(self, url: URL) -> httpcore.SyncHTTPTransport:
def _transport_for_url(self, request: Request) -> httpcore.SyncHTTPTransport:
"""
Returns the transport instance that should be used for a given URL.
This will either be the standard connection pool, or a proxy.
"""
enforce_http_url(url)
url = request.url
enforce_http_url(request)

if self._proxies and not should_not_be_proxied(url):
for matcher, transport in self._proxies.items():
Expand Down Expand Up @@ -590,7 +592,8 @@ def send(
timeout: typing.Union[TimeoutTypes, UnsetType] = UNSET,
) -> Response:
if request.url.scheme not in ("http", "https"):
raise InvalidURL('URL scheme must be "http" or "https".')
message = 'URL scheme must be "http" or "https".'
raise InvalidURL(message, request=request)

timeout = self.timeout if isinstance(timeout, UnsetType) else Timeout(timeout)

Expand Down Expand Up @@ -682,7 +685,7 @@ def _send_single_request(self, request: Request, timeout: Timeout) -> Response:
"""
Sends a single request, without handling any redirections.
"""
transport = self._transport_for_url(request.url)
transport = self._transport_for_url(request)

with map_exceptions(HTTPCORE_EXC_MAP, request=request):
(
Expand Down Expand Up @@ -1059,12 +1062,13 @@ def _init_proxy_transport(
http2=http2,
)

def _transport_for_url(self, url: URL) -> httpcore.AsyncHTTPTransport:
def _transport_for_url(self, request: Request) -> httpcore.AsyncHTTPTransport:
"""
Returns the transport instance that should be used for a given URL.
This will either be the standard connection pool, or a proxy.
"""
enforce_http_url(url)
url = request.url
enforce_http_url(request)

if self._proxies and not should_not_be_proxied(url):
for matcher, transport in self._proxies.items():
Expand Down Expand Up @@ -1204,7 +1208,7 @@ async def _send_single_request(
"""
Sends a single request, without handling any redirections.
"""
transport = self._transport_for_url(request.url)
transport = self._transport_for_url(request)

with map_exceptions(HTTPCORE_EXC_MAP, request=request):
(
Expand Down
41 changes: 26 additions & 15 deletions httpx/_decoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@
except ImportError: # pragma: nocover
brotli = None

if typing.TYPE_CHECKING: # pragma: no cover
from ._models import Request


class Decoder:
def __init__(self, request: "Request") -> None:
self.request = request

def decode(self, data: bytes) -> bytes:
raise NotImplementedError() # pragma: nocover

Expand All @@ -44,7 +50,8 @@ class DeflateDecoder(Decoder):
See: https://stackoverflow.com/questions/1838699
"""

def __init__(self) -> None:
def __init__(self, request: "Request") -> None:
self.request = request
self.first_attempt = True
self.decompressor = zlib.decompressobj()

Expand All @@ -57,13 +64,13 @@ def decode(self, data: bytes) -> bytes:
if was_first_attempt:
self.decompressor = zlib.decompressobj(-zlib.MAX_WBITS)
return self.decode(data)
raise DecodingError from exc
raise DecodingError(message=str(exc), request=self.request)

def flush(self) -> bytes:
try:
return self.decompressor.flush()
except zlib.error as exc: # pragma: nocover
raise DecodingError from exc
raise DecodingError(message=str(exc), request=self.request)


class GZipDecoder(Decoder):
Expand All @@ -73,20 +80,21 @@ class GZipDecoder(Decoder):
See: https://stackoverflow.com/questions/1838699
"""

def __init__(self) -> None:
def __init__(self, request: "Request") -> None:
self.request = request
self.decompressor = zlib.decompressobj(zlib.MAX_WBITS | 16)

def decode(self, data: bytes) -> bytes:
try:
return self.decompressor.decompress(data)
except zlib.error as exc:
raise DecodingError from exc
raise DecodingError(message=str(exc), request=self.request)

def flush(self) -> bytes:
try:
return self.decompressor.flush()
except zlib.error as exc: # pragma: nocover
raise DecodingError from exc
raise DecodingError(message=str(exc), request=self.request)


class BrotliDecoder(Decoder):
Expand All @@ -99,10 +107,11 @@ class BrotliDecoder(Decoder):
name. The top branches are for 'brotlipy' and bottom branches for 'Brotli'
"""

def __init__(self) -> None:
def __init__(self, request: "Request") -> None:
assert (
brotli is not None
), "The 'brotlipy' or 'brotli' library must be installed to use 'BrotliDecoder'"
self.request = request
self.decompressor = brotli.Decompressor()
self.seen_data = False
if hasattr(self.decompressor, "decompress"):
Expand All @@ -117,7 +126,7 @@ def decode(self, data: bytes) -> bytes:
try:
return self._decompress(data)
except brotli.error as exc:
raise DecodingError from exc
raise DecodingError(message=str(exc), request=self.request)

def flush(self) -> bytes:
if not self.seen_data:
Expand All @@ -127,7 +136,7 @@ def flush(self) -> bytes:
self.decompressor.finish()
return b""
except brotli.error as exc: # pragma: nocover
raise DecodingError from exc
raise DecodingError(message=str(exc), request=self.request)


class MultiDecoder(Decoder):
Expand Down Expand Up @@ -160,7 +169,8 @@ class TextDecoder:
Handles incrementally decoding bytes into text
"""

def __init__(self, encoding: typing.Optional[str] = None):
def __init__(self, request: "Request", encoding: typing.Optional[str] = None):
self.request = request
self.decoder: typing.Optional[codecs.IncrementalDecoder] = (
None if encoding is None else codecs.getincrementaldecoder(encoding)()
)
Expand Down Expand Up @@ -194,8 +204,8 @@ def decode(self, data: bytes) -> str:
self.buffer = None

return text
except UnicodeDecodeError: # pragma: nocover
raise DecodingError() from None
except UnicodeDecodeError as exc: # pragma: nocover
raise DecodingError(message=str(exc), request=self.request)

def flush(self) -> str:
try:
Expand All @@ -207,14 +217,15 @@ def flush(self) -> str:
return bytes(self.buffer).decode(self._detector_result())

return self.decoder.decode(b"", True)
except UnicodeDecodeError: # pragma: nocover
raise DecodingError() from None
except UnicodeDecodeError as exc: # pragma: nocover
raise DecodingError(message=str(exc), request=self.request)

def _detector_result(self) -> str:
self.detector.close()
result = self.detector.result["encoding"]
if not result: # pragma: nocover
raise DecodingError("Unable to determine encoding of content")
message = "Unable to determine encoding of content"
raise DecodingError(message, request=self.request)

return result

Expand Down
Loading

0 comments on commit 9409900

Please sign in to comment.