I use the python requests library to make HTTP requests. Handling exceptions and giving the non-technical end user a friendly message can be a challenge when the original exception is wrapped up in an exception chain. For example:
import requests
url = "http://one.two.threeFourFiveSixSevenEight"
try:
resp = requests.get(url)
except requests.RequestException as e:
print("Couldn't contact", url, ":", e)
Prints:
Couldn’t contact http://one.two.threeFourFiveSixSevenEight : HTTPConnectionPool(host=’one.two.threeFourFiveSixSevenEight’, port=80): Max retries exceeded with url: / (Caused by NewConnectionError(‘<requests.packages.urllib3.connection.HTTPConnection object at 0x7f527329c978>: Failed to establish a new connection: [Errno -2] Name or service not known’,))
And that’s a mouthful.
I want to tell the end user that DNS isn’t working, rather than showing the ugly stringified error message. How do I do that, in python3? Are python3 exceptions iterable? No. So I searched the internet, and found inspiration from the raven project. I adapted their code in two different ways to give me the result I wanted.
Update Aug 10: See the end of this blog post for a more elegant solution.
import socket
import requests
import sys
def chained_exceptions(exc_info=None):
"""
Adapted from: https://github.com/getsentry/raven-python/pull/811/files?diff=unified
Return a generator iterator over an exception's chain.
The exceptions are yielded from outermost to innermost (i.e. last to
first when viewing a stack trace).
"""
if not exc_info or exc_info is True:
exc_info = sys.exc_info()
if not exc_info:
raise ValueError("No exception found")
yield exc_info
exc_type, exc, exc_traceback = exc_info
while True:
if exc.__suppress_context__:
# Then __cause__ should be used instead.
exc = exc.__cause__
else:
exc = exc.__context__
if exc is None:
break
yield type(exc), exc, exc.__traceback__
def chained_exception_types(e=None):
"""
Return a generator iterator of exception types in the exception chain
The exceptions are yielded from outermost to innermost (i.e. last to
first when viewing a stack trace).
Adapted from: https://github.com/getsentry/raven-python/pull/811/files?diff=unified
"""
if not e or e is True:
e = sys.exc_info()[1]
if not e:
raise ValueError("No exception found")
yield type(e)
while True:
if e.__suppress_context__:
# Then __cause__ should be used instead.
e = e.__cause__
else:
e = e.__context__
if e is None:
break
yield type(e)
saved_exception = None
try:
resp = requests.get("http://one.two.threeFourFiveSixSevenEight")
except Exception as e:
saved_exception = e
if socket.gaierror in chained_exception_types(e):
print("Found socket.gaierror in exception block via e")
if socket.gaierror in chained_exception_types():
print("Found socket.gaierror in exception block via traceback")
if socket.gaierror in chained_exception_types(True):
print("Found socket.gaierror in exception block via traceback")
if saved_exception:
print("\nIterating exception chain for a saved exception...")
for t, ex, tb in chained_exceptions((type(saved_exception), saved_exception, saved_exception.__traceback__)):
print("\ttype:", t, "Exception:", ex)
if t == socket.gaierror:
print("\t*** Found socket.gaierror:", ex)
if socket.gaierror in chained_exception_types(saved_exception):
print("\t*** Found socket.gaierror via chained_exception_types")
Here’s the output:
Found socket.gaierror in exception block via e
Found socket.gaierror in exception block via traceback
Found socket.gaierror in exception block via traceback
Iterating exception chain for a saved exception...
type: <class 'requests.exceptions.ConnectionError'> Exception: HTTPConnectionPool(host='one.two.threeFourFiveSixSevenEight', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0x7fae7d0bfa20>: Failed to establish a new connection: [Errno -2] Name or service not known',))
type: <class 'requests.packages.urllib3.exceptions.MaxRetryError'> Exception: HTTPConnectionPool(host='one.two.threeFourFiveSixSevenEight', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0x7fae7d0bfa20>: Failed to establish a new connection: [Errno -2] Name or service not known',))
type: <class 'requests.packages.urllib3.exceptions.NewConnectionError'> Exception: <requests.packages.urllib3.connection.HTTPConnection object at 0x7fae7d0bfa20>: Failed to establish a new connection: [Errno -2] Name or service not known
type: <class 'socket.gaierror'> Exception: [Errno -2] Name or service not known
*** Found socket.gaierror: [Errno -2] Name or service not known
*** Found socket.gaierror via chained_exception_types()
Now I can write the following code:
url = "http://one.two.threeFourFiveSixSevenEight"
try:
resp = requests.get(url)
except requests.RequestException as e:
if socket.gaierror in chained_exception_types(e):
print("Couldn't get IP address for hostname in URL", url, " -- connect device to Internet")
else:
raise
Very nice — just what I wanted.
Note that Python 2 does not support exception chaining, so this only works in Python 3.
Aug 10: A colleague of mine, Lance Anderson, came up with a far more elegant solution:
import requests
import socket
class IterableException(object):
def __init__(self, ex):
self.ex = ex
def __iter__(self):
self.next = self.ex
return self
def __next__(self):
if self.next.__suppress_context__:
self.next = self.next.__cause__
else:
self.next = self.next.__context__
if self.next:
return self.next
else:
raise StopIteration
url = "http://one.two.threeFourFiveSixSevenEight"
try:
resp = requests.get(url)
except requests.RequestException as e:
ie = IterableException(e)
if socket.gaierror in [type(x) for x in ie]:
print("Couldn't get IP address for hostname in URL", url, " -- connect device to Internet.")