diff --git a/.python-version b/.python-version new file mode 100644 index 0000000000..8d64ba7a55 --- /dev/null +++ b/.python-version @@ -0,0 +1,5 @@ +3.9.20 +3.10.15 +3.11.10 +3.12.7 +3.13.9 diff --git a/botocore/parsers.py b/botocore/parsers.py index 9f1e6049d1..b3a5d5a2bf 100644 --- a/botocore/parsers.py +++ b/botocore/parsers.py @@ -592,7 +592,16 @@ def _handle_blob(self, shape, text): class QueryParser(BaseXMLResponseParser): def _do_error_parse(self, response, shape): xml_contents = response['body'] - root = self._parse_xml_string_to_dom(xml_contents) + try: + root = self._parse_xml_string_to_dom(xml_contents) + except ResponseParserError as e: + status_code = response.get('status_code') + if status_code and status_code in http.client.responses: + status_message = http.client.responses[status_code] + raise ResponseParserError( + f"{str(e)} (HTTP {status_code}: {status_message})" + ) + raise parsed = self._build_name_to_xml_node(root) self._replace_nodes(parsed) # Once we've converted xml->dict, we need to make one or two @@ -1447,7 +1456,16 @@ def _parse_error_from_http_status(self, response): def _parse_error_from_body(self, response): xml_contents = response['body'] - root = self._parse_xml_string_to_dom(xml_contents) + try: + root = self._parse_xml_string_to_dom(xml_contents) + except ResponseParserError as e: + status_code = response.get('status_code') + if status_code and status_code in http.client.responses: + status_message = http.client.responses[status_code] + raise ResponseParserError( + f"{str(e)} (HTTP {status_code}: {status_message})" + ) + raise parsed = self._build_name_to_xml_node(root) self._replace_nodes(parsed) if root.tag == 'Error': diff --git a/tests/unit/test_parsers.py b/tests/unit/test_parsers.py index b2cb425805..a0d2c7eab3 100644 --- a/tests/unit/test_parsers.py +++ b/tests/unit/test_parsers.py @@ -1621,6 +1621,54 @@ def test_can_parse_route53_with_missing_message(self): # still populate an empty string. self.assertEqual(error['Message'], '') + def test_query_parser_empty_body_4xx_error_with_notes(self): + parser = parsers.QueryParser() + response = { + 'body': b'', + 'headers': { + 'Content-Length': '0', + 'Date': 'Fri, 21 Nov 2025 18:18:28 GMT', + 'Connection': 'close', + }, + 'status_code': 413, + } + + with self.assertRaises(parsers.ResponseParserError) as cm: + parser._do_error_parse(response, None) + + exception = cm.exception + self.assertIn("HTTP 413:", str(exception)) + error_msg = str(exception) + self.assertTrue( + "Request Entity Too Large" in error_msg + or "Content Too Large" in error_msg, + f"Expected HTTP 413 message not found in: {error_msg}", + ) + + def test_parse_error_from_body_empty_body_4xx_error_with_notes(self): + parser = parsers.RestXMLParser() + response = { + 'body': b'', + 'headers': { + 'Content-Length': '0', + 'Date': 'Fri, 21 Nov 2025 18:18:28 GMT', + 'Connection': 'close', + }, + 'status_code': 413, + } + + with self.assertRaises(parsers.ResponseParserError) as cm: + parser._parse_error_from_body(response) + + exception = cm.exception + self.assertIn("HTTP 413:", str(exception)) + error_msg = str(exception) + self.assertTrue( + "Request Entity Too Large" in error_msg + or "Content Too Large" in error_msg, + f"Expected HTTP 413 message not found in: {error_msg}", + ) + def _generic_test_bodies(): generic_html_body = (