Skip to content

Commit 9409900

Browse files
Exception hierarchy (#1095)
* Exception heirachy * Exception heirarchy * Formatting tweaks * Update httpx/_exceptions.py Co-authored-by: Florimond Manca <florimond.manca@gmail.com> * Update httpx/_exceptions.py Co-authored-by: Florimond Manca <florimond.manca@gmail.com> * Update httpx/_exceptions.py Co-authored-by: Florimond Manca <florimond.manca@gmail.com> * Update httpx/_exceptions.py Co-authored-by: Florimond Manca <florimond.manca@gmail.com> Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
1 parent 2ba9c1e commit 9409900

11 files changed

Lines changed: 218 additions & 108 deletions

File tree

httpx/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,16 @@
1919
ProxyError,
2020
ReadError,
2121
ReadTimeout,
22-
RedirectError,
2322
RequestBodyUnavailable,
23+
RequestError,
2424
RequestNotRead,
2525
ResponseClosed,
2626
ResponseNotRead,
2727
StreamConsumed,
2828
StreamError,
2929
TimeoutException,
3030
TooManyRedirects,
31+
TransportError,
3132
WriteError,
3233
WriteTimeout,
3334
)
@@ -76,7 +77,7 @@
7677
"ProtocolError",
7778
"ReadError",
7879
"ReadTimeout",
79-
"RedirectError",
80+
"RequestError",
8081
"RequestBodyUnavailable",
8182
"ResponseClosed",
8283
"ResponseNotRead",
@@ -87,6 +88,7 @@
8788
"ProxyError",
8889
"TimeoutException",
8990
"TooManyRedirects",
91+
"TransportError",
9092
"WriteError",
9193
"WriteTimeout",
9294
"URL",

httpx/_auth.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,20 +116,24 @@ def auth_flow(self, request: Request) -> typing.Generator[Request, Response, Non
116116
# need to build an authenticated request.
117117
return
118118

119-
header = response.headers["www-authenticate"]
120-
challenge = self._parse_challenge(header)
119+
challenge = self._parse_challenge(request, response)
121120
request.headers["Authorization"] = self._build_auth_header(request, challenge)
122121
yield request
123122

124-
def _parse_challenge(self, header: str) -> "_DigestAuthChallenge":
123+
def _parse_challenge(
124+
self, request: Request, response: Response
125+
) -> "_DigestAuthChallenge":
125126
"""
126127
Returns a challenge from a Digest WWW-Authenticate header.
127128
These take the form of:
128129
`Digest realm="realm@host.com",qop="auth,auth-int",nonce="abc",opaque="xyz"`
129130
"""
131+
header = response.headers["www-authenticate"]
132+
130133
scheme, _, fields = header.partition(" ")
131134
if scheme.lower() != "digest":
132-
raise ProtocolError("Header does not start with 'Digest'")
135+
message = "Header does not start with 'Digest'"
136+
raise ProtocolError(message, request=request)
133137

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

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

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

222227
return header_value
223228

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

234-
raise ProtocolError(f'Unexpected qop value "{qop!r}" in digest auth')
241+
message = f'Unexpected qop value "{qop!r}" in digest auth'
242+
raise ProtocolError(message, request=request)
235243

236244

237245
class _DigestAuthChallenge:

httpx/_client.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,8 @@ def _redirect_url(self, request: Request, response: Response) -> URL:
324324

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

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

540-
def _transport_for_url(self, url: URL) -> httpcore.SyncHTTPTransport:
541+
def _transport_for_url(self, request: Request) -> httpcore.SyncHTTPTransport:
541542
"""
542543
Returns the transport instance that should be used for a given URL.
543544
This will either be the standard connection pool, or a proxy.
544545
"""
545-
enforce_http_url(url)
546+
url = request.url
547+
enforce_http_url(request)
546548

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

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

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

687690
with map_exceptions(HTTPCORE_EXC_MAP, request=request):
688691
(
@@ -1059,12 +1062,13 @@ def _init_proxy_transport(
10591062
http2=http2,
10601063
)
10611064

1062-
def _transport_for_url(self, url: URL) -> httpcore.AsyncHTTPTransport:
1065+
def _transport_for_url(self, request: Request) -> httpcore.AsyncHTTPTransport:
10631066
"""
10641067
Returns the transport instance that should be used for a given URL.
10651068
This will either be the standard connection pool, or a proxy.
10661069
"""
1067-
enforce_http_url(url)
1070+
url = request.url
1071+
enforce_http_url(request)
10681072

10691073
if self._proxies and not should_not_be_proxied(url):
10701074
for matcher, transport in self._proxies.items():
@@ -1204,7 +1208,7 @@ async def _send_single_request(
12041208
"""
12051209
Sends a single request, without handling any redirections.
12061210
"""
1207-
transport = self._transport_for_url(request.url)
1211+
transport = self._transport_for_url(request)
12081212

12091213
with map_exceptions(HTTPCORE_EXC_MAP, request=request):
12101214
(

httpx/_decoders.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@
1616
except ImportError: # pragma: nocover
1717
brotli = None
1818

19+
if typing.TYPE_CHECKING: # pragma: no cover
20+
from ._models import Request
21+
1922

2023
class Decoder:
24+
def __init__(self, request: "Request") -> None:
25+
self.request = request
26+
2127
def decode(self, data: bytes) -> bytes:
2228
raise NotImplementedError() # pragma: nocover
2329

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

47-
def __init__(self) -> None:
53+
def __init__(self, request: "Request") -> None:
54+
self.request = request
4855
self.first_attempt = True
4956
self.decompressor = zlib.decompressobj()
5057

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

6269
def flush(self) -> bytes:
6370
try:
6471
return self.decompressor.flush()
6572
except zlib.error as exc: # pragma: nocover
66-
raise DecodingError from exc
73+
raise DecodingError(message=str(exc), request=self.request)
6774

6875

6976
class GZipDecoder(Decoder):
@@ -73,20 +80,21 @@ class GZipDecoder(Decoder):
7380
See: https://stackoverflow.com/questions/1838699
7481
"""
7582

76-
def __init__(self) -> None:
83+
def __init__(self, request: "Request") -> None:
84+
self.request = request
7785
self.decompressor = zlib.decompressobj(zlib.MAX_WBITS | 16)
7886

7987
def decode(self, data: bytes) -> bytes:
8088
try:
8189
return self.decompressor.decompress(data)
8290
except zlib.error as exc:
83-
raise DecodingError from exc
91+
raise DecodingError(message=str(exc), request=self.request)
8492

8593
def flush(self) -> bytes:
8694
try:
8795
return self.decompressor.flush()
8896
except zlib.error as exc: # pragma: nocover
89-
raise DecodingError from exc
97+
raise DecodingError(message=str(exc), request=self.request)
9098

9199

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

102-
def __init__(self) -> None:
110+
def __init__(self, request: "Request") -> None:
103111
assert (
104112
brotli is not None
105113
), "The 'brotlipy' or 'brotli' library must be installed to use 'BrotliDecoder'"
114+
self.request = request
106115
self.decompressor = brotli.Decompressor()
107116
self.seen_data = False
108117
if hasattr(self.decompressor, "decompress"):
@@ -117,7 +126,7 @@ def decode(self, data: bytes) -> bytes:
117126
try:
118127
return self._decompress(data)
119128
except brotli.error as exc:
120-
raise DecodingError from exc
129+
raise DecodingError(message=str(exc), request=self.request)
121130

122131
def flush(self) -> bytes:
123132
if not self.seen_data:
@@ -127,7 +136,7 @@ def flush(self) -> bytes:
127136
self.decompressor.finish()
128137
return b""
129138
except brotli.error as exc: # pragma: nocover
130-
raise DecodingError from exc
139+
raise DecodingError(message=str(exc), request=self.request)
131140

132141

133142
class MultiDecoder(Decoder):
@@ -160,7 +169,8 @@ class TextDecoder:
160169
Handles incrementally decoding bytes into text
161170
"""
162171

163-
def __init__(self, encoding: typing.Optional[str] = None):
172+
def __init__(self, request: "Request", encoding: typing.Optional[str] = None):
173+
self.request = request
164174
self.decoder: typing.Optional[codecs.IncrementalDecoder] = (
165175
None if encoding is None else codecs.getincrementaldecoder(encoding)()
166176
)
@@ -194,8 +204,8 @@ def decode(self, data: bytes) -> str:
194204
self.buffer = None
195205

196206
return text
197-
except UnicodeDecodeError: # pragma: nocover
198-
raise DecodingError() from None
207+
except UnicodeDecodeError as exc: # pragma: nocover
208+
raise DecodingError(message=str(exc), request=self.request)
199209

200210
def flush(self) -> str:
201211
try:
@@ -207,14 +217,15 @@ def flush(self) -> str:
207217
return bytes(self.buffer).decode(self._detector_result())
208218

209219
return self.decoder.decode(b"", True)
210-
except UnicodeDecodeError: # pragma: nocover
211-
raise DecodingError() from None
220+
except UnicodeDecodeError as exc: # pragma: nocover
221+
raise DecodingError(message=str(exc), request=self.request)
212222

213223
def _detector_result(self) -> str:
214224
self.detector.close()
215225
result = self.detector.result["encoding"]
216226
if not result: # pragma: nocover
217-
raise DecodingError("Unable to determine encoding of content")
227+
message = "Unable to determine encoding of content"
228+
raise DecodingError(message, request=self.request)
218229

219230
return result
220231

0 commit comments

Comments
 (0)