Skip to content

Commit

Permalink
Fix Sqs ErrorType not being parsed properly (#3054)
Browse files Browse the repository at this point in the history
* Fix Sqs ErrorType not being parsed properly

* Isolate query compatible changes into function

---------

Co-authored-by: Nate Prewitt <[email protected]>
  • Loading branch information
nateprewitt authored Nov 11, 2023
2 parents 01dabf4 + 0d6c1fb commit 50861b9
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 11 deletions.
5 changes: 4 additions & 1 deletion botocore/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -975,7 +975,10 @@ def _make_api_call(self, operation_name, api_params):
)

if http.status_code >= 300:
error_code = parsed_response.get("Error", {}).get("Code")
error_info = parsed_response.get("Error", {})
error_code = error_info.get("QueryErrorCode") or error_info.get(
"Code"
)
error_class = self.exceptions.from_code(error_code)
raise error_class(parsed_response, operation_name)
else:
Expand Down
31 changes: 21 additions & 10 deletions botocore/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,26 +701,37 @@ def _do_error_parse(self, response, shape):
# if the message did not contain an error code
# include the response status code
response_code = response.get('status_code')
# Error response may contain an x-amzn-query-error header for json
# we need to fetch the error code from this header in that case
query_error = headers.get('x-amzn-query-error', '')
query_error_components = query_error.split(';')
code = None
if len(query_error_components) == 2 and query_error_components[0]:
code = query_error_components[0]
error['Error']['Type'] = query_error_components[1]
if code is None:
code = body.get('__type', response_code and str(response_code))

code = body.get('__type', response_code and str(response_code))
if code is not None:
# code has a couple forms as well:
# * "com.aws.dynamodb.vAPI#ProvisionedThroughputExceededException"
# * "ResourceNotFoundException"
if '#' in code:
code = code.rsplit('#', 1)[1]
if 'x-amzn-query-error' in headers:
code = self._do_query_compatible_error_parse(
code, headers, error
)
error['Error']['Code'] = code
self._inject_response_metadata(error, response['headers'])
return error

def _do_query_compatible_error_parse(self, code, headers, error):
"""
Error response may contain an x-amzn-query-error header to translate
errors codes from former `query` services into `json`. We use this to
do our lookup in the errorfactory for modeled errors.
"""
query_error = headers['x-amzn-query-error']
query_error_components = query_error.split(';')

if len(query_error_components) == 2 and query_error_components[0]:
error['Error']['QueryErrorCode'] = code
error['Error']['Type'] = query_error_components[1]
return query_error_components[0]
return code

def _inject_response_metadata(self, parsed, headers):
if 'x-amzn-requestid' in headers:
parsed.setdefault('ResponseMetadata', {})['RequestId'] = headers[
Expand Down
48 changes: 48 additions & 0 deletions tests/functional/test_sqs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# 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.

import pytest

from tests import BaseSessionTest, ClientHTTPStubber


class BaseSQSOperationTest(BaseSessionTest):
def setUp(self):
super().setUp()
self.region = "us-west-2"
self.client = self.session.create_client("sqs", self.region)
self.http_stubber = ClientHTTPStubber(self.client)


class SQSQueryCompatibleTest(BaseSQSOperationTest):
def test_query_compatible_error_parsing(self):
"""When migrating SQS from the ``query`` protocol to ``json``,
we unintentionally moved from modeled errors to a general ``ClientError``.
This ensures we're not silently regressing that behavior.
"""

error_body = (
b'{"__type":"com.amazonaws.sqs#QueueDoesNotExist",'
b'"message":"The specified queue does not exist."}'
)
error_headers = {
"x-amzn-query-error": "AWS.SimpleQueueService.NonExistentQueue;Sender",
}
with self.http_stubber as stub:
stub.add_response(
status=400, body=error_body, headers=error_headers
)
with pytest.raises(self.client.exceptions.QueueDoesNotExist):
self.client.delete_queue(
QueueUrl="not-a-real-queue-botocore",
)
6 changes: 6 additions & 0 deletions tests/unit/test_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,9 @@ def test_response_with_query_error_for_json_protocol(self):
self.assertEqual(
parsed['Error']['Code'], 'AWS.SimpleQueueService.NonExistentQueue'
)
self.assertEqual(
parsed['Error']['QueryErrorCode'], "ValidationException"
)
self.assertEqual(parsed['Error']['Type'], 'Sender')

def test_response_with_invalid_query_error_for_json_protocol(self):
Expand All @@ -1155,6 +1158,7 @@ def test_response_with_invalid_query_error_for_json_protocol(self):
self.assertIn('Error', parsed)
self.assertEqual(parsed['Error']['Message'], 'this is a message')
self.assertEqual(parsed['Error']['Code'], 'ValidationException')
self.assertNotIn('QueryErrorCode', parsed['Error'])
self.assertNotIn('Type', parsed['Error'])

def test_response_with_incomplete_query_error_for_json_protocol(self):
Expand All @@ -1177,6 +1181,7 @@ def test_response_with_incomplete_query_error_for_json_protocol(self):
self.assertIn('Error', parsed)
self.assertEqual(parsed['Error']['Message'], 'this is a message')
self.assertEqual(parsed['Error']['Code'], 'ValidationException')
self.assertNotIn('QueryErrorCode', parsed['Error'])
self.assertNotIn('Type', parsed['Error'])

def test_response_with_empty_query_errors_for_json_protocol(self):
Expand All @@ -1199,6 +1204,7 @@ def test_response_with_empty_query_errors_for_json_protocol(self):
self.assertIn('Error', parsed)
self.assertEqual(parsed['Error']['Message'], 'this is a message')
self.assertEqual(parsed['Error']['Code'], 'ValidationException')
self.assertNotIn('QueryErrorCode', parsed['Error'])
self.assertNotIn('Type', parsed['Error'])

def test_parse_error_response_for_query_protocol(self):
Expand Down

0 comments on commit 50861b9

Please sign in to comment.