Explicitly chain or don't chain exceptions
Explicitly chain exceptions (or disable the chaining) instead of
creating implicit "During handling of the above exception, another
exception occurred" which look like a bug.
MODIFY
src/urllib3/connection.py
src/urllib3/connection.py
@@ -201,15 +201,17 @@ class HTTPConnection(_HTTPConnection):
socket_options=self.socket_options,
)
except socket.gaierror as e:
- raise NameResolutionError(self.host, self, e)
- except SocketTimeout:
+ raise NameResolutionError(self.host, self, e) from e
+ except SocketTimeout as e:
raise ConnectTimeoutError(
self,
f"Connection to {self.host} timed out. (connect timeout={self.timeout})",
- )
+ ) from e
except OSError as e:
- raise NewConnectionError(self, f"Failed to establish a new connection: {e}")
+ raise NewConnectionError(
+ self, f"Failed to establish a new connection: {e}"
+ ) from e
return conn
@@ -583,10 +585,11 @@ class HTTPSConnection(HTTPConnection):
)
except Exception as e:
# Wrap into an HTTPSProxyError for easier diagnosis.
- # Original exception is available on original_error
+ # Original exception is available on original_error and
+ # as __cause__.
raise HTTPSProxyError(
f"Unable to establish a TLS connection to {hostname}", e
- )
+ ) from e
def _match_hostname(cert: _TYPE_PEER_CERT_RET, asserted_hostname: str) -> None:

MODIFY
src/urllib3/connectionpool.py
src/urllib3/connectionpool.py
@@ -263,14 +263,14 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
conn = self.pool.get(block=self.block, timeout=timeout)
except AttributeError: # self.pool is None
- raise ClosedPoolError(self, "Pool is closed.") # Defensive:
+ raise ClosedPoolError(self, "Pool is closed.") from None # Defensive:
except queue.Empty:
if self.block:
raise EmptyPoolError(
self,
"Pool is empty and a new connection can't be opened due to blocking mode.",
- )
+ ) from None
pass # Oh well, we'll create a new connection then
# If this is a persistent connection, check if it got disconnected
@@ -317,7 +317,7 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
raise FullPoolError(
self,
"Pool reached maximum size and no more connections are allowed.",
- )
+ ) from None
log.warning(
"Connection pool is full, discarding connection: %s", self.host
@@ -360,13 +360,13 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods):
if isinstance(err, SocketTimeout):
raise ReadTimeoutError(
self, url, f"Read timed out. (read timeout={timeout_value})"
- )
+ ) from err
# See the above comment about EAGAIN in Python 3.
if hasattr(err, "errno") and err.errno in _blocking_errnos:
raise ReadTimeoutError(
self, url, f"Read timed out. (read timeout={timeout_value})"
- )
+ ) from err
def _make_request(
self,

MODIFY
src/urllib3/contrib/_securetransport/bindings.py
src/urllib3/contrib/_securetransport/bindings.py
@@ -417,7 +417,7 @@ try:
CoreFoundation.CFDictionaryRef = CFDictionaryRef
except AttributeError:
- raise ImportError("Error initializing ctypes")
+ raise ImportError("Error initializing ctypes") from None
class CFConst:

MODIFY
src/urllib3/contrib/_securetransport/low_level.py
src/urllib3/contrib/_securetransport/low_level.py
@@ -109,7 +109,7 @@ def _create_cfstring_array(lst: List[bytes]) -> CFMutableArray:
except BaseException as e:
if cf_arr:
CoreFoundation.CFRelease(cf_arr)
- raise ssl.SSLError(f"Unable to allocate array: {e}")
+ raise ssl.SSLError(f"Unable to allocate array: {e}") from None
return cf_arr

MODIFY
src/urllib3/contrib/pyopenssl.py
src/urllib3/contrib/pyopenssl.py
@@ -297,21 +297,21 @@ class WrappedSocket:
if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"):
return b""
else:
- raise OSError(e.args[0], str(e))
+ raise OSError(e.args[0], str(e)) from e
except OpenSSL.SSL.ZeroReturnError:
if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:
return b""
else:
raise
- except OpenSSL.SSL.WantReadError:
+ except OpenSSL.SSL.WantReadError as e:
if not util.wait_for_read(self.socket, self.socket.gettimeout()):
- raise timeout("The read operation timed out") # type: ignore[arg-type]
+ raise timeout("The read operation timed out") from e # type: ignore[arg-type]
else:
return self.recv(*args, **kwargs)
# TLS 1.3 post-handshake authentication
except OpenSSL.SSL.Error as e:
- raise ssl.SSLError(f"read error: {e!r}")
+ raise ssl.SSLError(f"read error: {e!r}") from e
else:
return data # type: ignore[no-any-return]
@@ -322,21 +322,21 @@ class WrappedSocket:
if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"):
return 0
else:
- raise OSError(e.args[0], str(e))
+ raise OSError(e.args[0], str(e)) from e
except OpenSSL.SSL.ZeroReturnError:
if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN:
return 0
else:
raise
- except OpenSSL.SSL.WantReadError:
+ except OpenSSL.SSL.WantReadError as e:
if not util.wait_for_read(self.socket, self.socket.gettimeout()):
- raise timeout("The read operation timed out") # type: ignore[arg-type]
+ raise timeout("The read operation timed out") from e # type: ignore[arg-type]
else:
return self.recv_into(*args, **kwargs)
# TLS 1.3 post-handshake authentication
except OpenSSL.SSL.Error as e:
- raise ssl.SSLError(f"read error: {e!r}")
+ raise ssl.SSLError(f"read error: {e!r}") from e
def settimeout(self, timeout: float) -> None:
return self.socket.settimeout(timeout)
@@ -345,12 +345,12 @@ class WrappedSocket:
while True:
try:
return self.connection.send(data) # type: ignore[no-any-return]
- except OpenSSL.SSL.WantWriteError:
+ except OpenSSL.SSL.WantWriteError as e:
if not util.wait_for_write(self.socket, self.socket.gettimeout()):
- raise timeout()
+ raise timeout() from e
continue
except OpenSSL.SSL.SysCallError as e:
- raise OSError(e.args[0], str(e))
+ raise OSError(e.args[0], str(e)) from e
def sendall(self, data: bytes) -> None:
total_sent = 0
@@ -448,7 +448,7 @@ class PyOpenSSLContext:
if cadata is not None:
self._ctx.load_verify_locations(BytesIO(cadata))
except OpenSSL.SSL.Error as e:
- raise ssl.SSLError(f"unable to load trusted certificates: {e!r}")
+ raise ssl.SSLError(f"unable to load trusted certificates: {e!r}") from e
def load_cert_chain(
self,
@@ -488,12 +488,12 @@ class PyOpenSSLContext:
while True:
try:
cnx.do_handshake()
- except OpenSSL.SSL.WantReadError:
+ except OpenSSL.SSL.WantReadError as e:
if not util.wait_for_read(sock, sock.gettimeout()):
- raise timeout("select timed out") # type: ignore[arg-type]
+ raise timeout("select timed out") from e # type: ignore[arg-type]
continue
except OpenSSL.SSL.Error as e:
- raise ssl.SSLError(f"bad handshake: {e!r}")
+ raise ssl.SSLError(f"bad handshake: {e!r}") from e
break
return WrappedSocket(cnx, sock)

MODIFY
src/urllib3/contrib/securetransport.py
src/urllib3/contrib/securetransport.py
@@ -385,9 +385,11 @@ class WrappedSocket:
if trust_result in successes:
return
reason = f"error code: {int(trust_result)}"
+ exc = None
except Exception as e:
# Do not trust on error
reason = f"exception: {e!r}"
+ exc = e
# SecureTransport does not send an alert nor shuts down the connection.
rec = _build_tls_unknown_ca_alert(self.version())
@@ -398,7 +400,7 @@ class WrappedSocket:
opts = struct.pack("ii", 1, 0)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, opts)
self.close()
- raise ssl.SSLError(f"certificate verify failed, {reason}")
+ raise ssl.SSLError(f"certificate verify failed, {reason}") from exc
def _evaluate_trust(self, trust_bundle: bytes) -> int:
# We want data in memory, so load it up.

MODIFY
src/urllib3/contrib/socks.py
src/urllib3/contrib/socks.py
@@ -120,11 +120,11 @@ class SOCKSConnection(HTTPConnection):
**extra_kw,
)
- except SocketTimeout:
+ except SocketTimeout as e:
raise ConnectTimeoutError(
self,
f"Connection to {self.host} timed out. (connect timeout={self.timeout})",
- )
+ ) from e
except socks.ProxyError as e:
# This is fragile as hell, but it seems to be the only way to raise
@@ -135,18 +135,20 @@ class SOCKSConnection(HTTPConnection):
raise ConnectTimeoutError(
self,
f"Connection to {self.host} timed out. (connect timeout={self.timeout})",
- )
+ ) from e
else:
raise NewConnectionError(
self, f"Failed to establish a new connection: {error}"
- )
+ ) from e
else:
raise NewConnectionError(
self, f"Failed to establish a new connection: {e}"
- )
+ ) from e
except OSError as e: # Defensive: PySocks should catch all these.
- raise NewConnectionError(self, f"Failed to establish a new connection: {e}")
+ raise NewConnectionError(
+ self, f"Failed to establish a new connection: {e}"
+ ) from e
return conn

MODIFY
src/urllib3/exceptions.py
src/urllib3/exceptions.py
@@ -66,6 +66,7 @@ class SSLError(HTTPError):
class ProxyError(HTTPError):
"""Raised when the connection to a proxy fails."""
+ # The original error is also available as __cause__.
original_error: Exception
def __init__(self, message: str, error: Exception) -> None:

MODIFY
src/urllib3/response.py
src/urllib3/response.py
@@ -314,7 +314,7 @@ class BaseHTTPResponse(io.IOBase):
"Received response with content-encoding: %s, but "
"failed to decode it." % content_encoding,
e,
- )
+ ) from e
if flush_decoder:
data += self._flush_decoder()
@@ -565,22 +565,22 @@ class HTTPResponse(BaseHTTPResponse):
try:
yield
- except SocketTimeout:
+ except SocketTimeout as e:
# FIXME: Ideally we'd like to include the url in the ReadTimeoutError but
# there is yet no clean way to get at it from this context.
- raise ReadTimeoutError(self._pool, None, "Read timed out.") # type: ignore[arg-type]
+ raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type]
except BaseSSLError as e:
# FIXME: Is there a better way to differentiate between SSLErrors?
if "read operation timed out" not in str(e):
# SSL errors related to framing/MAC get wrapped and reraised here
- raise SSLError(e)
+ raise SSLError(e) from e
- raise ReadTimeoutError(self._pool, None, "Read timed out.") # type: ignore[arg-type]
+ raise ReadTimeoutError(self._pool, None, "Read timed out.") from e # type: ignore[arg-type]
except (HTTPException, OSError) as e:
# This includes IncompleteRead.
- raise ProtocolError(f"Connection broken: {e!r}", e)
+ raise ProtocolError(f"Connection broken: {e!r}", e) from e
# If no exception is thrown, we should avoid cleaning up
# unnecessarily.
@@ -804,7 +804,7 @@ class HTTPResponse(BaseHTTPResponse):
except ValueError:
# Invalid chunked protocol response, abort.
self.close()
- raise InvalidChunkLength(self, line)
+ raise InvalidChunkLength(self, line) from None
def _handle_chunk(self, amt: Optional[int]) -> bytes:
returned_chunk = None

MODIFY
src/urllib3/util/request.py
src/urllib3/util/request.py
@@ -138,10 +138,10 @@ def rewind_body(body: IO[AnyStr], body_pos: Optional[Union[int, object]]) -> Non
if body_seek is not None and isinstance(body_pos, int):
try:
body_seek(body_pos)
- except OSError:
+ except OSError as e:
raise UnrewindableBodyError(
"An error occurred when rewinding request body for redirect/retry."
- )
+ ) from e
elif body_pos is _FAILEDTELL:
raise UnrewindableBodyError(
"Unable to record file position for rewinding "

MODIFY
src/urllib3/util/retry.py
src/urllib3/util/retry.py
@@ -493,7 +493,8 @@ class Retry:
)
if new_retry.is_exhausted():
- raise MaxRetryError(_pool, url, error or ResponseError(cause)) # type: ignore[arg-type]
+ reason = error or ResponseError(cause)
+ raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type]
log.debug("Incremented Retry for (url='%s'): %r", url, new_retry)

MODIFY
src/urllib3/util/ssl_.py
src/urllib3/util/ssl_.py
@@ -379,7 +379,7 @@ def ssl_wrap_socket(
try:
context.load_verify_locations(ca_certs, ca_cert_dir, ca_cert_data)
except OSError as e:
- raise SSLError(e)
+ raise SSLError(e) from e
elif ssl_context is None and hasattr(context, "load_default_certs"):
# try to load OS default certs; works well on Windows.

MODIFY
src/urllib3/util/timeout.py
src/urllib3/util/timeout.py
@@ -150,7 +150,7 @@ class Timeout:
raise ValueError(
"Timeout value %s was %s, but it must be an "
"int, float or None." % (name, value)
- )
+ ) from None
try:
if value <= 0: # type: ignore[operator]
@@ -164,7 +164,7 @@ class Timeout:
raise ValueError(
"Timeout value %s was %s, but it must be an "
"int, float or None." % (name, value)
- )
+ ) from None
return value

MODIFY
test/with_dummyserver/test_socketlevel.py
test/with_dummyserver/test_socketlevel.py
@@ -482,7 +482,7 @@ class TestSocketClosing(SocketDummyServerTestCase):
# second ReadTimeoutError due to errno
with HTTPSConnectionPool(host=self.host):
- err = mock.Mock()
+ err = OSError()
err.errno = errno.EAGAIN
with pytest.raises(ReadTimeoutError):
pool._raise_timeout(err, "", 0)

Explicitly chain or don't chain exceptions (#2340)

Explicitly chain exceptions (or disable the chaining) instead of
creating implicit "During handling of the above exception, another
exception occurred" which look like a bug.

Co-authored-by: Seth Michael Larson <sethmichaellarson@gmail.com>