откуда происходит SSL ConnectionResetError?

TL; DR

Мой вопрос прост - где код, ответственный за повышение ConnectionResetError на cpython3 после вызова self._sslobj.read(len, buffer) на ssl.py?

Фон

Иногда я получаю ConnectionResetError при попытке подключиться к S3 с помощью ssl. эта ошибка возникает редко, поэтому ее сложно воспроизвести.

# trimmed stacktrace
File "/MYPROJECT/MY_FUNC.py", line 123, in <genexpr>
rows = (row for row in reader)
File "/XXX/lib/python3.6/csv.py", line 112, in _next_
row = next(self.reader)
File "/XXX/lib/python3.6/tarfile.py", line 706, in readinto
buf = self.read(len(b))
File "/XXX/lib/python3.6/tarfile.py", line 695, in read
b = self.fileobj.read(length)
File "/XXX/lib/python3.6/gzip.py", line 276, in read
return self._buffer.read(size)
File "/XXX/lib/python3.6/_compression.py", line 68, in readinto
data = self.read(len(byte_view))
File "/XXX/lib/python3.6/gzip.py", line 469, in read
buf = self._fp.read(io.DEFAULT_BUFFER_SIZE)
File "/XXX/lib/python3.6/gzip.py", line 91, in read
self.file.read(size-self._length+read)
File "/XXX/lib/python3.6/site-packages/s3fs/core.py", line 1311, in read
self._fetch(self.loc, self.loc + length)
File "/XXX/lib/python3.6/site-packages/s3fs/core.py", line 1292, in _fetch
req_kw=self.s3.req_kw)
File "/XXX/lib/python3.6/site-packages/s3fs/core.py", line 1496, in _fetch_range
return resp['Body'].read()
File "/XXX/lib/python3.6/site-packages/botocore/response.py", line 74, in read
chunk = self._raw_stream.read(amt)
File "/XXX/lib/python3.6/site-packages/botocore/vendored/requests/packages/urllib3/response.py", line 239, in read
data = self._fp.read()
File "/XXX/lib/python3.6/http/client.py", line 462, in read
s = self._safe_read(self.length)
File "/XXX/lib/python3.6/http/client.py", line 612, in _safe_read
chunk = self.fp.read(min(amt, MAXAMOUNT))
File "/XXX/lib/python3.6/socket.py", line 586, in readinto
return self._sock.recv_into(b)
File "/XXX/lib/python3.6/ssl.py", line 1009, in recv_into
return self.read(nbytes, buffer)
File "/XXX/lib/python3.6/ssl.py", line 871, in read
return self._sslobj.read(len, buffer)
File "/XXX/lib/python3.6/ssl.py", line 631, in read
v = self._sslobj.read(len, buffer)
ConnectionResetError: [Errno 104] Connection reset by peer

Что я пробовал

глядя на ssl.py:631 я не ssl.py:631 дальнейших подсказок - нам нужно идти глубже !:

    def read(self, len=1024, buffer=None):
    """Read up to 'len' bytes from the SSL object and return them.

    If 'buffer' is provided, read into this buffer and return the number of
    bytes read.
    """
    if buffer is not None:
        v = self._sslobj.read(len, buffer)  # <--- exception here
    else:
        v = self._sslobj.read(len)
    return v

Я попытался найти его на CPython-репо, но AFAICS ничего, похоже, не поднимет, я подозреваю, что он скрыт в реализации SSL или на некотором сопоставлении между OSError и подклассами ConnectionError.

моя конечная цель - написать py2 & py3-совместимый код для обработки этих исключений (ConnectionError является новым на py3), сравнивая версии модулей py2 & py3, которые поднимают эту ошибку.


Обновление - py2 & PY3 улов для ConnectionError подклассов

мой вопрос возник, чтобы найти способ поймать ConnectionError и его подклассы на python2 & python3, так что вот оно:

import errno

# ref: https://docs.python.org/3/library/exceptions.html#ConnectionError
_CONNECTION_ERRORS = frozenset({
    errno.ECONNRESET,  # ConnectionResetError
    errno.EPIPE, errno.ESHUTDOWN,  # BrokenPipeError
    errno.ECONNABORTED,  # ConnectionAbortedError
    errno.ECONNREFUSED,  # ConnectionRefusedError
})

try:
    ...
except OSError as e:
    if e.errno not in _CONNECTION_ERRORS:
        raise
    print('got ConnectionError - %e' % e)

Ответ 1

ConnectionResetError возникает, когда errno является ECONNRESET. errno - это то, как libc указывает, произошла ли ошибка в системном вызове.

Вы можете найти ConnectionResetError в Objects/exceptions.c чтобы узнать, как этот тип исключения инициализируется и добавляется в errnomap dict.

В случае self._sslobj.read, _sslobj.read ConnectionResetError, _sslobj.read реализуется с помощью _ssl__SSLSocket_read_impl, фактическое чтение ssl выполняется с помощью openssl SSL_read:

count = SSL_read(self->ssl, mem, len);
_PySSL_UPDATE_ERRNO_IF(count <= 0, self, count);

по мере _PySSL_UPDATE_ERRNO_IF ошибки _PySSL_UPDATE_ERRNO_IF установит (sock)->ssl_errno = SSL_ERROR_SYSCALL и (sock)->c_errno = ECONNRESET.

позже, в PySSL_SetError:

    err = obj->ssl_errno;
    switch (err) {
    ...
    case SSL_ERROR_SYSCALL:

        if (obj->c_errno) {
            errno = obj->c_errno;
            return PyErr_SetFromErrno(PyExc_OSError);
        }

PyErr_SetFromErrno(PyExc_OSError) равен:

OSError(errno.ECONNRESET, 'Connection reset by peer', ...)

когда OSError конструирует с помощью errno, он будет искать более определенный подкласс, путем поиска значения errno в вышеупомянутом errnomap dict:

newtype = PyDict_GetItem(errnomap, myerrno);
if (newtype) {
    assert(PyType_Check(newtype));
    type = (PyTypeObject *) newtype;
}

он фактически возвращает и вызывает исключение ConnectionResetError.