Repository: boto/boto

Issue:
CloudFormation errors have JSON bodies but boto expects XML
I'm seeing that exceptions thrown when there are CloudFormation errors do not have useful error messages. Reading the code in https://github.com/boto/boto/blob/develop/boto/exception.py, I see that it tries to parse the error's body as XML. If parsing fails, it stores the body away, so I am able to retrieve it and see that it's actually JSON.

Here's an example, where I pass on both the original exception (with `None` in body) and the JSON. 

```
>>> import boto.cloudformation
>>> try:
...     boto.cloudformation.connect_to_region('eu-west-1').create_stack('some-stack-name')
... except boto.exception.BotoServerError, e:
...     raise Exception (e, e.message)
... 
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
Exception: (BotoServerError: 400 Bad Request
None, '{"Error":{"Code":"ValidationError","Message":"Either Template URL or Template Body must be specified.","Type":"Sender"},"RequestId":"470cf56b-8e7b-11e3-9695-09b9ac925fa5"}')
```



Discussion:

Resolving patch:
diff --git a/boto/cloudformation/connection.py b/boto/cloudformation/connection.py
--- a/boto/cloudformation/connection.py
+++ b/boto/cloudformation/connection.py
@@ -259,7 +259,7 @@ def _do_request(self, call, params, path, method):
         else:
             boto.log.error('%s %s' % (response.status, response.reason))
             boto.log.error('%s' % body)
-            raise self.ResponseError(response.status, response.reason, body)
+            raise self.ResponseError(response.status, response.reason, body=body)
 
     def create_stack(self, stack_name, template_body=None, template_url=None,
             parameters=None, notification_arns=None, disable_rollback=None,
diff --git a/boto/exception.py b/boto/exception.py
--- a/boto/exception.py
+++ b/boto/exception.py
@@ -27,6 +27,7 @@
 import base64
 import xml.sax
 from boto import handler
+from boto.compat import json
 from boto.resultset import ResultSet
 
 
@@ -88,12 +89,25 @@ def __init__(self, status, reason, body=None, *args):
                 h = handler.XmlHandlerWrapper(self, self)
                 h.parseString(self.body)
             except (TypeError, xml.sax.SAXParseException), pe:
-                # Remove unparsable message body so we don't include garbage
-                # in exception. But first, save self.body in self.error_message
-                # because occasionally we get error messages from Eucalyptus
-                # that are just text strings that we want to preserve.
-                self.message = self.body
-                self.body = None
+                # What if it's JSON? Let's try that.
+                try:
+                    parsed = json.loads(self.body)
+
+                    if 'RequestId' in parsed:
+                        self.request_id = parsed['RequestId']
+                    if 'Error' in parsed:
+                        if 'Code' in parsed['Error']:
+                            self.error_code = parsed['Error']['Code']
+                        if 'Message' in parsed['Error']:
+                            self.message = parsed['Error']['Message']
+
+                except ValueError:
+                    # Remove unparsable message body so we don't include garbage
+                    # in exception. But first, save self.body in self.error_message
+                    # because occasionally we get error messages from Eucalyptus
+                    # that are just text strings that we want to preserve.
+                    self.message = self.body
+                    self.body = None
 
     def __getattr__(self, name):
         if name == 'error_message':


Test patch:
diff --git a/tests/unit/cloudformation/test_connection.py b/tests/unit/cloudformation/test_connection.py
--- a/tests/unit/cloudformation/test_connection.py
+++ b/tests/unit/cloudformation/test_connection.py
@@ -11,6 +11,7 @@
 
 from tests.unit import AWSMockServiceTestCase
 from boto.cloudformation.connection import CloudFormationConnection
+from boto.exception import BotoServerError
 
 
 SAMPLE_TEMPLATE = r"""
@@ -108,12 +109,24 @@ def test_create_stack_with_minimum_args(self):
 
     def test_create_stack_fails(self):
         self.set_http_response(status_code=400, reason='Bad Request',
-                               body='Invalid arg.')
-        with self.assertRaises(self.service_connection.ResponseError):
+            body='{"Error": {"Code": 1, "Message": "Invalid arg."}}')
+        with self.assertRaisesRegexp(self.service_connection.ResponseError,
+            'Invalid arg.'):
             api_response = self.service_connection.create_stack(
                 'stack_name', template_body=SAMPLE_TEMPLATE,
                 parameters=[('KeyName', 'myKeyName')])
 
+    def test_create_stack_fail_error(self):
+        self.set_http_response(status_code=400, reason='Bad Request',
+            body='{"RequestId": "abc", "Error": {"Code": 1, "Message": "Invalid arg."}}')
+        try:
+            api_response = self.service_connection.create_stack(
+                'stack_name', template_body=SAMPLE_TEMPLATE,
+                parameters=[('KeyName', 'myKeyName')])
+        except BotoServerError, e:
+            self.assertEqual('abc', e.request_id)
+            self.assertEqual(1, e.error_code)
+            self.assertEqual('Invalid arg.', e.message)
 
 class TestCloudFormationUpdateStack(CloudFormationConnectionBase):
     def default_body(self):