From b8b583d6e367a5452a41a53dbd8f18ea87816a59 Mon Sep 17 00:00:00 2001 From: om-ghante Date: Wed, 27 May 2026 20:43:56 +0530 Subject: [PATCH] Fix TypeError in s3 error handler when Message is None S3-compatible endpoints (Ceph, MinIO, Hetzner) may return XML error responses with empty elements. Python's ElementTree parses these as None, causing botocore to set Error.Message to None in the parsed response dict. The _is_sigv4_error_message() and _is_kms_sigv4_error_message() functions use the in operator to check if a substring exists in the message, which raises TypeError when the message is None. Use or-coalescing to normalize None values to empty strings before performing containment checks. Also guard the enhance_error_msg() body against None Message and missing Endpoint in the PermanentRedirect path. Fixes #10161 --- .changes/next-release/s3-bugfix-10161.json | 7 ++ awscli/customizations/s3errormsg.py | 18 ++--- tests/unit/customizations/test_s3errormsg.py | 74 ++++++++++++++++++++ 3 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 .changes/next-release/s3-bugfix-10161.json diff --git a/.changes/next-release/s3-bugfix-10161.json b/.changes/next-release/s3-bugfix-10161.json new file mode 100644 index 000000000000..49a8d06ae3f7 --- /dev/null +++ b/.changes/next-release/s3-bugfix-10161.json @@ -0,0 +1,7 @@ +[ + { + "category": "``s3``", + "description": "Fix ``TypeError`` in S3 error message handler when the error response contains a ``None`` ``Message`` value, which can occur with S3-compatible endpoints that return empty ```` XML elements.", + "type": "bugfix" + } +] diff --git a/awscli/customizations/s3errormsg.py b/awscli/customizations/s3errormsg.py index a7a0b9eb4f32..cfd94155683f 100644 --- a/awscli/customizations/s3errormsg.py +++ b/awscli/customizations/s3errormsg.py @@ -44,24 +44,26 @@ def enhance_error_msg(parsed, **kwargs): message += REGION_ERROR_MSG parsed['Error']['Message'] = message elif _is_permanent_redirect_message(parsed): - endpoint = parsed['Error']['Endpoint'] - message = parsed['Error']['Message'] + endpoint = parsed['Error'].get('Endpoint', '') + message = parsed['Error'].get('Message') or '' new_message = message[:-1] + ': %s\n' % endpoint new_message += REGION_ERROR_MSG parsed['Error']['Message'] = new_message elif _is_kms_sigv4_error_message(parsed): - parsed['Error']['Message'] += ENABLE_SIGV4_MSG + current_msg = parsed['Error'].get('Message') or '' + parsed['Error']['Message'] = current_msg + ENABLE_SIGV4_MSG def _is_sigv4_error_message(parsed): - return ('Please use AWS4-HMAC-SHA256' in - parsed.get('Error', {}).get('Message', '')) + error_message = parsed.get('Error', {}).get('Message') or '' + return 'Please use AWS4-HMAC-SHA256' in error_message def _is_permanent_redirect_message(parsed): - return parsed.get('Error', {}).get('Code', '') == 'PermanentRedirect' + error_code = parsed.get('Error', {}).get('Code') or '' + return error_code == 'PermanentRedirect' def _is_kms_sigv4_error_message(parsed): - return ('AWS KMS managed keys require AWS Signature Version 4' in - parsed.get('Error', {}).get('Message', '')) + error_message = parsed.get('Error', {}).get('Message') or '' + return 'AWS KMS managed keys require AWS Signature Version 4' in error_message diff --git a/tests/unit/customizations/test_s3errormsg.py b/tests/unit/customizations/test_s3errormsg.py index 0af91ac9a9a0..8a24c4d696b6 100644 --- a/tests/unit/customizations/test_s3errormsg.py +++ b/tests/unit/customizations/test_s3errormsg.py @@ -84,3 +84,77 @@ def test_not_an_error_message(self): s3errormsg.enhance_error_msg(parsed) # Nothing should have changed self.assertEqual(parsed, expected) + + def test_none_message_does_not_crash(self): + # Empty parsed as None must not raise TypeError. + parsed = { + 'Error': { + 'Code': 'AccessDenied', + 'Message': None, + } + } + # Should not raise TypeError + s3errormsg.enhance_error_msg(parsed) + # Message should remain None since no error pattern matched + self.assertIsNone(parsed['Error']['Message']) + + def test_none_message_with_sigv4_code(self): + # None Message should not match sigv4 pattern. + parsed = { + 'Error': { + 'Code': 'AuthorizationHeaderMalformed', + 'Message': None, + } + } + s3errormsg.enhance_error_msg(parsed) + # Should not have been enhanced (no match) + self.assertIsNone(parsed['Error']['Message']) + + def test_none_message_with_kms_context(self): + # None Message should not match KMS sigv4 pattern. + parsed = { + 'Error': { + 'Code': 'InvalidArgument', + 'Message': None, + } + } + s3errormsg.enhance_error_msg(parsed) + self.assertIsNone(parsed['Error']['Message']) + + def test_none_code_does_not_crash(self): + # None Code should not crash PermanentRedirect check. + parsed = { + 'Error': { + 'Code': None, + 'Message': 'Some error message.', + } + } + expected = copy.deepcopy(parsed) + s3errormsg.enhance_error_msg(parsed) + # Nothing should have changed + self.assertEqual(parsed, expected) + + def test_empty_string_message_does_not_crash(self): + # Empty string Message should be handled without crashing. + parsed = { + 'Error': { + 'Code': 'AccessDenied', + 'Message': '', + } + } + expected = copy.deepcopy(parsed) + s3errormsg.enhance_error_msg(parsed) + self.assertEqual(parsed, expected) + + def test_permanent_redirect_with_none_message(self): + # PermanentRedirect with None Message should not crash. + parsed = { + 'Error': { + 'Code': 'PermanentRedirect', + 'Message': None, + 'Endpoint': 'myendpoint', + } + } + s3errormsg.enhance_error_msg(parsed) + # Should have enhanced the message despite None original + self.assertIn('myendpoint', parsed['Error']['Message'])