Durable way to obtain SSLError errno

## Issue

I want to get the errno property of an SSLError object the right way.
## Description

I have a Session object, for which I call the request method made inside of a try-catch block. I would like to distinguish between different situations, all of which will have the same traceback, and all of which produce an SSLError.

Here is the specific code I'm working with, starting with the except block with SSLError:

https://github.com/ansible/tower-cli/blob/master/lib/tower_cli/api.py#L103

Different situations will return (at least) two different types of SSLErrors:
1. requests.exceptions.SSLError: EOF occurred in violation of protocol (_ssl.c:590)
2. requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:590)
## Work-around Attempts

Here is an example of someone else who tried to distinguish between these scenarios:

https://github.com/kisom/woofs/blob/ca589bf57390069f61981fdae1b216d8c657f75c/woofs.py#L237

``` python
                    if 'EOF occurred in violation of protocol' in str(e):
                            continue
```

(note: My exception is named `ex`, but it is named `e` in the above code)

I'm not a fan of this code. If I do pdb.set_trace() and inspect the error in my case, I find that `ex.errno` is `None`.
### EOF error

``` python
    (Pdb) print ex.args[0].args[0].args
    (1, u'[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:590)')
```
### CA error

``` python
    (Pdb) print ex.args[0].args[0].args
    (8, u'EOF occurred in violation of protocol (_ssl.c:590)')
```

You can see that the first error has an error number 1, and the second error has an error number 8. This is what I want.

So I could obtain the SSLError number from `ex.args[0].args[0].args[0]`, and branch different cases based on that. But if the requests library changes in the future, another layer of try-except might be added, so realistically I would need to write a loop that recursively reads `args` as long as it's a tuple, until I get to an integer. Is this the best available method to get the error number?

It seems strange that `ex.message` will automatically look into the args chain and obtain the error message string, but `ex.errno` will not do the same for the error number. I'm using requests 2.9.1.


This would be helpful as i get `SSLError: ("bad handshake: SysCallError(-1, 'Unexpected EOF')",)` when i run my scripts and i am not sure what error is causing this issue exactly. The traceback does lead to the Requests `adapter.py` library.


@girishsortur This would not help you at all. You can already see the errno (it's -1), and the error message ("unexpected EOF"). That means that the remote server terminated the connection without cleanly shutting down the TLS connection, likely midway through the handshake.


@Lukasa thanks. I wasn't sure if `-1` was the error code exactly. Is this error being caused from server end or from client machine? Any help or link that you can share which explains what this error is? Thanks


Unfortunately, OpenSSL is very bad at reporting errors. Likely this is caused by the server and client having an incompatible set of settings, and the server choosing to terminate the handshake. You can try looking at a [blog post I wrote](https://lukasa.co.uk/2016/01/Debugging_With_Wireshark_TLS/) and see if that helps you debug the problem.


@AlanCoding I think you're on the right track with that code. At the least, it seems like using `args[0]` as the original exception isn't going away anytime soon. The code that currently raises an `SSLError` [passes the original argument as the first parameter](https://github.com/kennethreitz/requests/blob/3f8b1fb617cfdd3911602b5c3e668ad30602c64d/requests/adapters.py#L447). If you look in that same file, there are lots of places where the original exception is passed as the first param.

That said, if you're lucky enough to use python3 you can use the `__context__` property to reliably get at the root cause of an exception and (I don't think) it can change despite whatever changes might happen in requests.

I've put together what I think is the best way to get what you're looking for.

``` py
import requests


def get_errno_py2k(ex):
    error_number = None
    current_error = ex
    while isinstance(current_error, Exception) and error_number is None:
        error_number = getattr(current_error, 'errno', None)
        current_error = current_error.args[0]

    return error_number


def get_errno_py3k(ex):
    error_number = None
    current_error = ex
    while current_error and error_number is None:
        error_number = getattr(current_error, 'errno', None)
        current_error = current_error.__context__

    return error_number

try:
    requests.get('https://expired.badssl.com/')
except requests.exceptions.SSLError as ex:
    get_errno = get_errno_py3k if hasattr(ex, '__context__') else get_errno_py2k
    errno = get_errno(ex)
    print(ex)
    print('SSL error number: {}'.format(errno))
```

And the results:

```
python2 test.py
[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:645)
SSL error number: 1

python3 test.py
[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:590)
SSL error number: 1
```

Thoughts?


In an effort to clean up the issue tracker to only have issues that are still relevant to the project we've done a quick pass and decided this issue may no longer be relevant for a variety of potential reasons, including:

* Applies to a much older version, unclear whether the issue still applies.
* Change requires a backwards incompatible release and it's unclear if the benefits are worth the migration effort from the community.
* There isn't a clear demand from the community on the change landing in Requests.

If you think the issue should remain open, please comment so below or open a new issue and link back to the original issue. Again, thank you for opening the issue and for the discussion, it's much appreciated.