Repository: boto/botocore
Pull request #707 (2015-10-30 20:46:19)

Wrap the BadStatusLine with a clear description
New test case is added. Also simplify the existing test cases a little bit.


Patch:
diff --git a/botocore/endpoint.py b/botocore/endpoint.py
index 8c62926b7..1f4c9375d 100644
--- a/botocore/endpoint.py
+++ b/botocore/endpoint.py
@@ -25,6 +25,7 @@ from botocore.vendored import six
 from botocore.awsrequest import create_request_object
 from botocore.exceptions import BaseEndpointResolverError
 from botocore.exceptions import EndpointConnectionError
+from botocore.exceptions import ConnectionClosedError
 from botocore.compat import filter_ssl_warnings
 from botocore.utils import is_valid_endpoint_url
 from botocore.hooks import first_non_none_response
@@ -178,6 +179,10 @@ class Endpoint(object):
                 better_exception = EndpointConnectionError(
                     endpoint_url=endpoint_url, error=e)
                 return (None, better_exception)
+            elif self._looks_like_bad_status_line(e):
+                better_exception = ConnectionClosedError(
+                    endpoint_url=e.request.url, request=e.request)
+                return (None, better_exception)
             else:
                 return (None, e)
         except Exception as e:
@@ -196,6 +201,9 @@ class Endpoint(object):
     def _looks_like_dns_error(self, e):
         return 'gaierror' in str(e) and e.request is not None
 
+    def _looks_like_bad_status_line(self, e):
+        return 'BadStatusLine' in str(e) and e.request is not None
+
     def _needs_retry(self, attempts, operation_model, response=None,
                      caught_exception=None):
         event_name = 'needs-retry.%s.%s' % (self._endpoint_prefix,
diff --git a/botocore/exceptions.py b/botocore/exceptions.py
index e34c416cd..3e9358235 100644
--- a/botocore/exceptions.py
+++ b/botocore/exceptions.py
@@ -11,6 +11,7 @@
 # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
 # ANY KIND, either express or implied. See the License for the specific
 # language governing permissions and limitations under the License.
+from botocore.vendored.requests.exceptions import ConnectionError
 
 
 class BotoCoreError(Exception):
@@ -19,7 +20,7 @@ class BotoCoreError(Exception):
 
     :ivar msg: The descriptive message associated with the error.
     """
-    fmt = 'An unspecified error occured'
+    fmt = 'An unspecified error occurred'
 
     def __init__(self, **kwargs):
         msg = self.fmt.format(**kwargs)
@@ -52,6 +53,16 @@ class EndpointConnectionError(BotoCoreError):
         'Could not connect to the endpoint URL: "{endpoint_url}"')
 
 
+class ConnectionClosedError(ConnectionError):
+    fmt = (
+        'Connection was closed before we received a valid response '
+        'from endpoint URL: "{endpoint_url}".')
+    def __init__(self, **kwargs):
+        msg = self.fmt.format(**kwargs)
+        kwargs.pop('endpoint_url')
+        super(ConnectionClosedError, self).__init__(msg, **kwargs)
+
+
 class NoCredentialsError(BotoCoreError):
     """
     No credentials could be found


Test patch:
diff --git a/tests/unit/test_endpoint.py b/tests/unit/test_endpoint.py
index a42f542b7..aeababf1f 100644
--- a/tests/unit/test_endpoint.py
+++ b/tests/unit/test_endpoint.py
@@ -22,6 +22,7 @@ from botocore.endpoint import Endpoint, DEFAULT_TIMEOUT
 from botocore.endpoint import EndpointCreator
 from botocore.endpoint import PreserveAuthSession
 from botocore.exceptions import EndpointConnectionError
+from botocore.exceptions import ConnectionClosedError
 from botocore.exceptions import BaseEndpointResolverError
 
 
@@ -99,12 +100,6 @@ class TestEndpointFeatures(TestEndpointBase):
         self.assertNotIn('Authorization', prepared_request.headers)
 
     def test_make_request_no_signature_version(self):
-        self.endpoint = Endpoint(
-            'https://ec2.us-west-2.amazonaws.com/',
-            endpoint_prefix='ec2',
-            event_emitter=self.event_emitter)
-        self.endpoint.http_session = self.http_session
-
         self.endpoint.make_request(self.op, request_dict())
 
         # http_session should be used to send the request.
@@ -113,10 +108,6 @@ class TestEndpointFeatures(TestEndpointBase):
         self.assertNotIn('Authorization', prepared_request.headers)
 
     def test_make_request_injects_better_dns_error_msg(self):
-        self.endpoint = Endpoint(
-            'https://ec2.us-west-2.amazonaws.com/',
-            endpoint_prefix='ec2', event_emitter=self.event_emitter)
-        self.endpoint.http_session = self.http_session
         fake_request = Mock(url='https://ec2.us-west-2.amazonaws.com')
         self.http_session.send.side_effect = ConnectionError(
             "Fake gaierror(8, node or host not known)", request=fake_request)
@@ -124,6 +115,15 @@ class TestEndpointFeatures(TestEndpointBase):
                                      'Could not connect'):
             self.endpoint.make_request(self.op, request_dict())
 
+    def test_make_request_injects_better_bad_status_line_error_msg(self):
+        fake_request = Mock(url='https://ec2.us-west-2.amazonaws.com')
+        self.http_session.send.side_effect = ConnectionError(
+            """'Connection aborted.', BadStatusLine("''",)""",
+            request=fake_request)
+        with self.assertRaisesRegexp(ConnectionClosedError,
+                                     'Connection was closed'):
+            self.endpoint.make_request(self.op, request_dict())
+
 
 class TestRetryInterface(TestEndpointBase):
     def setUp(self):