Sam Hooke

Get errno from Python requests `ConnectionError`

Using the Python requests library, I wanted to make a PUT request, and ignore any ConnectionError if it was an error 104 “Connection reset by peer”. (104 is ECONNRESET defined in errno.h). If this error was raised, the traceback I received was as follows:

[...]
  File "/usr/bob/venv/lib/python2.7/site-packages/requests/sessions.py", line 518, in request
    resp = self.send(prep, **send_kwargs)
  File "/usr/bob/venv/lib/python2.7/site-packages/requests/sessions.py", line 639, in send
    r = adapter.send(request, **kwargs)
  File "/usr/bob/venv/lib/python2.7/site-packages/requests/adapters.py", line 488, in send
    raise ConnectionError(err, request=request)
ConnectionError: ('Connection aborted.', error(104, 'Connection reset by peer'))

Using dir(requests.ConnectionError) showed that it had an errno attribute, and so I naïvely tried this solution first:

try:
    result = requests.put(url, json=payload)
    result.raise_for_status()
except requests.ConnectionError as err:
    # WRONG
    if not err.errno == 104:
        raise

This did not work. The exception was always raised.

It turned out that err.errno was None. (This appears to be because err.errno actually comes from IOError, the standard Python class from which ConnectionError inherits).

Tenacious use of dir revealed that the correct rune is not err.errno, but err.args[0].args[1].errno. This process of discovery can be seen in the log below:

>>> dir(err)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__getitem__', '__getslice__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', '__unicode__', '__weakref__', 'args', 'errno', 'filename', 'message', 'request', 'response', 'strerror']
>>> dir(err.args[0])
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__getitem__', '__getslice__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', '__unicode__', '__weakref__', 'args', 'message']
>>> err.args[0].args
('Connection aborted.', error(104, 'Connection reset by peer'))
>>> err.args[0].message
>>> dir(err.args[0].args)
['__add__', '__class__', '__contains__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getslice__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'index']
>>> dir(err.args[0].message)
['__add__', '__class__', '__contains__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getslice__', '__gt__', '__hash__', '__init__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_formatter_field_name_split', '_formatter_parser', 'capitalize', 'center', 'count', 'decode', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'index', 'isalnum', 'isalpha', 'isdigit', 'islower', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
>>> err.args[0].args[1]
[Errno 104] Connection reset by peer
>>> err.args[0].args[1].errno
104

So finally, to ignore ConnectionError 104:

try:
    result = requests.put(url, json=payload)
    result.raise_for_status()
except requests.ConnectionError as err:
    # RIGHT -- though will these attributes and indices will always exist?
    if not err.args[0].args[1].errno == 104:
        raise