diff --git a/HISTORY.md b/HISTORY.md index 0b0521c9ca..f712a84fb3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,6 +6,11 @@ dev - \[Short description of non-trivial change.\] +**Bugfixes** + +- Fixed urllib3 exception leak, wrapping `urllib3.exceptions.SSLError` with + `requests.exceptions.SSLError` for `content` and `iter_content`. + 2.27.1 (2022-01-05) ------------------- diff --git a/requests/models.py b/requests/models.py index dfbea854f9..f65afa858f 100644 --- a/requests/models.py +++ b/requests/models.py @@ -19,7 +19,12 @@ from urllib3.filepost import encode_multipart_formdata from urllib3.util import parse_url from urllib3.exceptions import ( - DecodeError, ReadTimeoutError, ProtocolError, LocationParseError) + DecodeError, + LocationParseError, + ProtocolError, + ReadTimeoutError, + SSLError, +) from io import UnsupportedOperation from .hooks import default_hooks @@ -32,6 +37,7 @@ ContentDecodingError, ConnectionError, StreamConsumedError, InvalidJSONError) from .exceptions import JSONDecodeError as RequestsJSONDecodeError +from .exceptions import SSLError as RequestsSSLError from ._internal_utils import to_native_string, unicode_is_ascii from .utils import ( guess_filename, get_auth_from_url, requote_uri, @@ -765,6 +771,8 @@ def generate(): raise ContentDecodingError(e) except ReadTimeoutError as e: raise ConnectionError(e) + except SSLError as e: + raise RequestsSSLError(e) else: # Standard file-like object. while True: diff --git a/tests/test_requests.py b/tests/test_requests.py index 328da4bd43..074c372a82 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -14,6 +14,7 @@ import io import requests import pytest +import urllib3 from requests.adapters import HTTPAdapter from requests.auth import HTTPDigestAuth, _basic_auth_str from requests.compat import ( @@ -22,9 +23,25 @@ from requests.cookies import ( cookiejar_from_dict, morsel_to_cookie) from requests.exceptions import ( - ConnectionError, ConnectTimeout, InvalidSchema, InvalidURL, - MissingSchema, ReadTimeout, Timeout, RetryError, RequestException, TooManyRedirects, - ProxyError, InvalidHeader, UnrewindableBodyError, SSLError, InvalidProxyURL, InvalidJSONError) + ChunkedEncodingError, + ConnectionError, + ConnectTimeout, + ContentDecodingError, + InvalidHeader, + InvalidJSONError, + InvalidProxyURL, + InvalidSchema, + InvalidURL, + MissingSchema, + ProxyError, + ReadTimeout, + RequestException, + RetryError, + Timeout, + TooManyRedirects, + UnrewindableBodyError, +) +from requests.exceptions import SSLError as RequestsSSLError from requests.models import PreparedRequest from requests.structures import CaseInsensitiveDict from requests.sessions import SessionRedirectMixin @@ -910,7 +927,7 @@ def test_certificate_failure(self, httpbin_secure): """ When underlying SSL problems occur, an SSLError is raised. """ - with pytest.raises(SSLError): + with pytest.raises(RequestsSSLError): # Our local httpbin does not have a trusted CA, so this call will # fail if we use our default trust bundle. requests.get(httpbin_secure('status', '200')) @@ -1320,6 +1337,26 @@ def test_response_chunk_size_type(self): with pytest.raises(TypeError): chunks = r.iter_content("1024") + @pytest.mark.parametrize( + 'exception, args, expected', ( + (urllib3.exceptions.ProtocolError, tuple(), ChunkedEncodingError), + (urllib3.exceptions.DecodeError, tuple(), ContentDecodingError), + (urllib3.exceptions.ReadTimeoutError, (None, '', ''), ConnectionError), + (urllib3.exceptions.SSLError, tuple(), RequestsSSLError), + ) + ) + def test_iter_content_wraps_exceptions( + self, httpbin, mocker, exception, args, expected + ): + r = requests.Response() + r.raw = mocker.Mock() + # ReadTimeoutError can't be initialized by mock + # so we'll manually create the instance with args + r.raw.stream.side_effect = exception(*args) + + with pytest.raises(expected): + next(r.iter_content(1024)) + def test_request_and_response_are_pickleable(self, httpbin): r = requests.get(httpbin('get'))