Repository: boto/botocore
Pull request #456 (2015-02-11 23:35:07)

Let exceptions propogate from make_request
This prevents a value of None from ever being returned
from make_request.  Previously, exceptions were caught and
after the retry logic would just return a value of None.

Now, if things like `ConnectionError`s, `socket.error`s, etc.
are raised, then this will propogate.  This also fixed the
client interface that expected that a tuple was always returned
from `make_request`.  This is now the case.

Noticed this while working on another bug fix.  Now instead
of:

```
  File "t.py", line 10, in <module>
    ContentDisposition="attachment; filename=5小時接力起跑.jpg;")
  File "botocore/botocore/client.py", line 292, in _api_call
    operation_model, request_dict)
TypeError: 'NoneType' object is not iterable
```

you'll get:

```
Traceback (most recent call last):
  File "t.py", line 10, in <module>
    ContentDisposition="attachment; filename=5小時接力起跑.jpg;")
  File "botocore/botocore/client.py", line 292, in _api_call
    operation_model, request_dict)
  File "botocore/botocore/endpoint.py", line 109, in make_request
    return self._send_request(request_dict, operation_model)
  File "botocore/botocore/endpoint.py", line 161, in _send_request
    raise exception
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 43: ordinal not in range(128)
```

Though keep in mind the above traceback is actually also a bug and will be fixed
shortly.

cc @danielgtaylor @kyleknap 


Patch:
diff --git a/botocore/endpoint.py b/botocore/endpoint.py
index eaae85da3..d5d9ec715 100644
--- a/botocore/endpoint.py
+++ b/botocore/endpoint.py
@@ -142,10 +142,10 @@ class Endpoint(object):
     def _send_request(self, request_dict, operation_model):
         attempts = 1
         request = self.create_request(request_dict, operation_model)
-        response, exception = self._get_response(
+        success_response, exception = self._get_response(
             request, operation_model, attempts)
         while self._needs_retry(attempts, operation_model,
-                                response, exception):
+                                success_response, exception):
             attempts += 1
             # If there is a stream associated with the request, we need
             # to reset it before attempting to send the request again.
@@ -155,11 +155,19 @@ class Endpoint(object):
             # Create a new request when retried (including a new signature).
             request = self.create_request(
                 request_dict, operation_model=operation_model)
-            response, exception = self._get_response(request, operation_model,
-                                                     attempts)
-        return response
+            success_response, exception = self._get_response(
+                request, operation_model, attempts)
+        if exception is not None:
+            raise exception
+        else:
+            return success_response
 
     def _get_response(self, request, operation_model, attempts):
+        # This will return a tuple of (success_response, exception)
+        # and success_response is itself a tuple of
+        # (http_response, parsed_dict).
+        # If an exception occurs then the success_response is None.
+        # If no exception occurs then exception is None.
         try:
             logger.debug("Sending http request: %s", request)
             http_response = self.http_session.send(


Test patch:
diff --git a/tests/unit/test_endpoint.py b/tests/unit/test_endpoint.py
index a81bcca5a..03b11fb0d 100644
--- a/tests/unit/test_endpoint.py
+++ b/tests/unit/test_endpoint.py
@@ -240,7 +240,8 @@ class TestRetryInterface(TestEndpointBase):
             [(None, None)]  # Check if retry needed. Retry not needed.
         ]
         self.http_session.send.side_effect = ConnectionError()
-        self.endpoint.make_request(op, request_dict())
+        with self.assertRaises(ConnectionError):
+            self.endpoint.make_request(op, request_dict())
         call_args = self.event_emitter.emit.call_args_list
         self.assertEqual(self.event_emitter.emit.call_count, 4)
         # Check that all of the events are as expected.