Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

from where SSL ConnectionResetError comes from?

TL;DR

My question is simple - where is the code responsible to raise ConnectionResetError on cpython3 following a call to self._sslobj.read(len, buffer) on ssl.py?

Background

I'm getting sometimes ConnectionResetError when trying to connect to S3 with ssl. this error occurs rarely so its tricky to reproduce it.

# 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

What i've tried

looking at ssl.py:631 gives me no further clues - we have to go deeper!:

    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

i've tried searching it on CPython repo but AFAICS nothing seems to raise it, i suspect its hidden in SSL implementation or on some mapping between OSError to ConnectionError subclasses.

my final goal is to write py2 & py3 compatible code for handling this exceptions (ConnectionError is new on py3) by comparing the module's py2 & py3 versions that raises this error.


Update - py2 & py3 catch for ConnectionError subclasses

my question origins was to find a way to catch ConnectionError and its subclasses on python2 & python3, so here it is:

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)
like image 397
ShmulikA Avatar asked Sep 13 '18 11:09

ShmulikA


1 Answers

ConnectionResetError is raised when errno is ECONNRESET. errno is how libc indicates whether or not an error occurred in a system call.

You could search ConnectionResetError in Objects/exceptions.c to find out how this exception type get initialized and added to errnomap dict.

In the case of self._sslobj.read raised ConnectionResetError, _sslobj.read is implemented with _ssl__SSLSocket_read_impl, the actual ssl read is done with openssl's SSL_read:

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

as the error occurred, _PySSL_UPDATE_ERRNO_IF will set (sock)->ssl_errno = SSL_ERROR_SYSCALL and (sock)->c_errno = ECONNRESET.

later, in 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) equals with:

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

when OSError constructs with an errno, it will lookup a more specified subclass, by lookup errno value in the aforementioned errnomap dict:

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

it actually returns and raises out a ConnectionResetError exception.

like image 200
georgexsh Avatar answered Oct 15 '22 10:10

georgexsh