Skip to content

Commit 63b1b27

Browse files
committed
Add HTTP status context to XML parsing errors using PEP 678 notes
When XML parsing fails due to empty or malformed responses, users now receive the actual HTTP error response from the service alongside the parsing error, providing useful information about what actually happened. Before: ResponseParserError: Unable to parse response (no element found: line 1, column 0), invalid XML received. Further retries may succeed: b'' After: ResponseParserError: Unable to parse response (no element found: line 1, column 0), invalid XML received. Further retries may succeed: b'' HTTP 413: Content Too Large This exposes the real service error (HTTP 413: Content Too Large) that was previously hidden behind cryptic XML parsing failures, giving users actionable information about why their request failed.
1 parent c660cc8 commit 63b1b27

2 files changed

Lines changed: 60 additions & 2 deletions

File tree

botocore/parsers.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,15 @@ def _handle_blob(self, shape, text):
592592
class QueryParser(BaseXMLResponseParser):
593593
def _do_error_parse(self, response, shape):
594594
xml_contents = response['body']
595-
root = self._parse_xml_string_to_dom(xml_contents)
595+
try:
596+
root = self._parse_xml_string_to_dom(xml_contents)
597+
except ResponseParserError as e:
598+
status_message = http.client.responses.get(
599+
response['status_code'], ''
600+
)
601+
if status_message:
602+
e.add_note(f"HTTP {response['status_code']}: {status_message}")
603+
raise
596604
parsed = self._build_name_to_xml_node(root)
597605
self._replace_nodes(parsed)
598606
# Once we've converted xml->dict, we need to make one or two
@@ -1447,7 +1455,15 @@ def _parse_error_from_http_status(self, response):
14471455

14481456
def _parse_error_from_body(self, response):
14491457
xml_contents = response['body']
1450-
root = self._parse_xml_string_to_dom(xml_contents)
1458+
try:
1459+
root = self._parse_xml_string_to_dom(xml_contents)
1460+
except ResponseParserError as e:
1461+
status_message = http.client.responses.get(
1462+
response['status_code'], ''
1463+
)
1464+
if status_message:
1465+
e.add_note(f"HTTP {response['status_code']}: {status_message}")
1466+
raise
14511467
parsed = self._build_name_to_xml_node(root)
14521468
self._replace_nodes(parsed)
14531469
if root.tag == 'Error':

tests/unit/test_parsers.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1621,6 +1621,48 @@ def test_can_parse_route53_with_missing_message(self):
16211621
# still populate an empty string.
16221622
self.assertEqual(error['Message'], '')
16231623

1624+
def test_query_parser_empty_body_4xx_error_with_notes(self):
1625+
parser = parsers.QueryParser()
1626+
response = {
1627+
'body': b'',
1628+
'headers': {
1629+
'Content-Length': '0',
1630+
'Date': 'Fri, 21 Nov 2025 18:18:28 GMT',
1631+
'Connection': 'close',
1632+
},
1633+
'status_code': 413,
1634+
}
1635+
1636+
with self.assertRaises(parsers.ResponseParserError) as cm:
1637+
parser._do_error_parse(response, None)
1638+
1639+
exception = cm.exception
1640+
1641+
self.assertTrue(hasattr(exception, '__notes__'))
1642+
self.assertEqual(len(exception.__notes__), 1)
1643+
self.assertEqual(exception.__notes__[0], "HTTP 413: Content Too Large")
1644+
1645+
def test_parse_error_from_body_empty_body_4xx_error_with_notes(self):
1646+
parser = parsers.RestXMLParser()
1647+
response = {
1648+
'body': b'',
1649+
'headers': {
1650+
'Content-Length': '0',
1651+
'Date': 'Fri, 21 Nov 2025 18:18:28 GMT',
1652+
'Connection': 'close',
1653+
},
1654+
'status_code': 413,
1655+
}
1656+
1657+
with self.assertRaises(parsers.ResponseParserError) as cm:
1658+
parser._parse_error_from_body(response)
1659+
1660+
exception = cm.exception
1661+
1662+
self.assertTrue(hasattr(exception, '__notes__'))
1663+
self.assertEqual(len(exception.__notes__), 1)
1664+
self.assertEqual(exception.__notes__[0], "HTTP 413: Content Too Large")
1665+
16241666

16251667
def _generic_test_bodies():
16261668
generic_html_body = (

0 commit comments

Comments
 (0)