Repository: urllib3/urllib3

Issue:
SSLError
urllib 1.25.6
```
Traceback (most recent call last):
  File "/home/web/web/yandex/.env/lib/python3.5/site-packages/ioweb/network_service.py", line 177, in thread_network
    req, res, error, raise_network_error=False
  File "/home/web/web/yandex/.env/lib/python3.5/site-packages/ioweb/transport.py", line 310, in prepare_response
    self.read_with_timeout(req, res)
  File "/home/web/web/yandex/.env/lib/python3.5/site-packages/ioweb/transport.py", line 266, in read_with_timeout
    chunk = self.urllib3_response.read(chunk_size)
  File "/home/web/web/yandex/.env/lib/python3.5/site-packages/urllib3/response.py", line 507, in read
    data = self._fp.read(amt) if not fp_closed else b""
  File "/usr/lib/python3.5/http/client.py", line 448, in read
    n = self.readinto(b)
  File "/usr/lib/python3.5/http/client.py", line 478, in readinto
    return self._readinto_chunked(b)
  File "/usr/lib/python3.5/http/client.py", line 573, in _readinto_chunked
    chunk_left = self._get_chunk_left()
  File "/usr/lib/python3.5/http/client.py", line 541, in _get_chunk_left
    chunk_left = self._read_next_chunk_size()
  File "/usr/lib/python3.5/http/client.py", line 501, in _read_next_chunk_size
    line = self.fp.readline(_MAXLINE + 1)
  File "/usr/lib/python3.5/socket.py", line 576, in readinto
    return self._sock.recv_into(b)
  File "/home/web/web/yandex/.env/lib/python3.5/site-packages/urllib3/contrib/pyopenssl.py", line 332, in recv_into
    raise ssl.SSLError("read error: %r" % e)
ssl.SSLError: ("read error: Error([('SSL routines', 'ssl3_get_record', 'decryption failed or bad record mac')],)",)
```
Is it OK?
Should not it be `exceptions.SSLError` or something like that derived from urllib3 package?


Discussion:
Hi @lorien!
Can you please submit a simple reproducing script?
I've seen this error as well.  Building a server that produces this message reliably sounds like a pain, but it should be clear from the stack trace that `urllib3` is allowing an SSLError to escape even though [lower level exceptions should be wrapped](https://urllib3.readthedocs.io/en/latest/user-guide.html#errors-exceptions).

Looking at the code here: https://github.com/urllib3/urllib3/blob/9971e27e83a891ba7b832fa9e5d2f04bbcb1e65f/src/urllib3/response.py#L437 it seems like if there is any SSLError besides a read timeout, it will end up hitting user code.
@christopher-hesse I believe you're correct, we should be wrapping the `BaseSSLError` into a `urllib3.exceptions.SSLError` in line with the `_error_catcher()` contract of wrapping low-level Python exceptions into urllib3 exceptions.
It looks like `urllib3.exceptions.SSLError` says `Raised when SSL certificate fails in an HTTPS connection.` so it might be worth changing that documentation, or else special-casing this particular error with `if "decryption failed or bad record mac" in str(e): raise ProtocolError(...)`

Resolving patch:
diff --git a/src/urllib3/response.py b/src/urllib3/response.py
--- a/src/urllib3/response.py
+++ b/src/urllib3/response.py
@@ -22,6 +22,7 @@
     InvalidChunkLength,
     InvalidHeader,
     HTTPError,
+    SSLError,
 )
 from .packages.six import string_types as basestring, PY3
 from .connection import HTTPException, BaseSSLError
@@ -443,9 +444,8 @@ def _error_catcher(self):
             except BaseSSLError as e:
                 # FIXME: Is there a better way to differentiate between SSLErrors?
                 if "read operation timed out" not in str(e):  # Defensive:
-                    # This shouldn't happen but just in case we're missing an edge
-                    # case, let's avoid swallowing SSL errors.
-                    raise
+                    # SSL errors related to framing/MAC get wrapped and reraised here
+                    raise SSLError(e)
 
                 raise ReadTimeoutError(self._pool, None, "Read timed out.")
 


Test patch:
diff --git a/test/test_response.py b/test/test_response.py
--- a/test/test_response.py
+++ b/test/test_response.py
@@ -1,7 +1,9 @@
 # -*- coding: utf-8 -*-
 
+import contextlib
 import re
 import socket
+import ssl
 import zlib
 
 from io import BytesIO, BufferedReader, TextIOWrapper
@@ -19,6 +21,7 @@
     httplib_IncompleteRead,
     IncompleteRead,
     InvalidChunkLength,
+    SSLError,
 )
 from urllib3.packages.six.moves import http_client as httplib
 from urllib3.util.retry import Retry, RequestHistory
@@ -936,6 +939,30 @@ def stream():
 
         assert b"foo\nbar" == data
 
+    def test_non_timeout_ssl_error_on_read(self):
+        mac_error = ssl.SSLError(
+            "SSL routines", "ssl3_get_record", "decryption failed or bad record mac"
+        )
+
+        @contextlib.contextmanager
+        def make_bad_mac_fp():
+            fp = BytesIO(b"")
+            with mock.patch.object(fp, "read") as fp_read:
+                # mac/decryption error
+                fp_read.side_effect = mac_error
+                yield fp
+
+        with make_bad_mac_fp() as fp:
+            with pytest.raises(SSLError) as e:
+                HTTPResponse(fp)
+            assert e.value.args[0] == mac_error
+
+        with make_bad_mac_fp() as fp:
+            resp = HTTPResponse(fp, preload_content=False)
+            with pytest.raises(SSLError) as e:
+                resp.read()
+            assert e.value.args[0] == mac_error
+
 
 class MockChunkedEncodingResponse(object):
     def __init__(self, content):