Skip to content

Commit f7ffe66

Browse files
authored
ASF: fix empty exception member serialization when required (#13186)
1 parent 692af03 commit f7ffe66

File tree

2 files changed

+146
-2
lines changed

2 files changed

+146
-2
lines changed

localstack-core/localstack/aws/protocol/serializer.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,7 +1338,13 @@ def _serialize_error(
13381338
else:
13391339
continue
13401340

1341-
if value:
1341+
if value is None:
1342+
# do not serialize a value that is set to `None`
1343+
continue
1344+
1345+
# if the value is falsy (empty string, empty list) and not in the Shape required members, AWS will
1346+
# not serialize it, and it will not be part of the response body.
1347+
if value or member in shape.required_members:
13421348
remaining_params[member] = value
13431349

13441350
self._serialize(body, remaining_params, shape, None, mime_type)
@@ -1845,7 +1851,13 @@ def _serialize_error_structure(
18451851
else:
18461852
continue
18471853

1848-
if value:
1854+
if value is None:
1855+
# do not serialize a value that is set to `None`
1856+
continue
1857+
1858+
# if the value is falsy (empty string, empty list) and not in the Shape required members, AWS will
1859+
# not serialize it, and it will not be part of the response body.
1860+
if value or member in shape.required_members:
18491861
params[member] = value
18501862

18511863
self._serialize_type_structure(body, params, shape, None, shape_members=shape_members)

tests/unit/aws/protocol/test_serializer.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,96 @@ def test_json_protocol_error_serialization_with_shaped_default_members_on_root()
764764
assert "message" not in parsed_body
765765

766766

767+
def test_json_protocol_error_serialization_empty_message():
768+
# if the exception message is not passed when instantiating the exception, the message attribute will be set to
769+
# an empty string "". This is not serialized if the message field is not a required member
770+
exception = TransactionCanceledException()
771+
772+
response = _botocore_error_serializer_integration_test(
773+
"dynamodb",
774+
"ExecuteTransaction",
775+
exception,
776+
"TransactionCanceledException",
777+
400,
778+
"",
779+
)
780+
assert "message" not in response
781+
782+
783+
@pytest.mark.parametrize("empty_value", ("", None))
784+
def test_json_protocol_error_serialization_with_empty_non_required_members(empty_value):
785+
class _ResourceNotFoundException(ServiceException):
786+
code: str = "ResourceNotFoundException"
787+
sender_fault: bool = True
788+
status_code: int = 404
789+
ResourceType: str | None
790+
ResourceId: str | None
791+
792+
exception = _ResourceNotFoundException("Not Found", ResourceType="")
793+
794+
response = _botocore_error_serializer_integration_test(
795+
"arc-region-switch",
796+
"ApprovePlanExecutionStep",
797+
exception,
798+
"ResourceNotFoundException",
799+
404,
800+
"Not Found",
801+
protocol="json",
802+
)
803+
assert "ResourceType" not in response
804+
805+
806+
def test_json_protocol_error_serialization_falsy_non_required_members():
807+
# if the exception message is not passed, an empty message will be passed as an empty string `""`
808+
exception = TransactionCanceledException("Exception message!", CancellationReasons=[])
809+
810+
response = _botocore_error_serializer_integration_test(
811+
"dynamodb",
812+
"ExecuteTransaction",
813+
exception,
814+
"TransactionCanceledException",
815+
400,
816+
"Exception message!",
817+
Message="Exception message!",
818+
)
819+
assert "CancellationReasons" not in response
820+
821+
822+
@pytest.mark.parametrize("empty_value", ("", None))
823+
def test_json_protocol_error_serialization_with_empty_required_members(empty_value):
824+
class _ResourceNotFoundException(ServiceException):
825+
code: str = "ResourceNotFoundException"
826+
sender_fault: bool = False
827+
status_code: int = 404
828+
resourceId: str
829+
resourceType: str
830+
831+
resource_type = "test"
832+
exception = _ResourceNotFoundException(
833+
"Exception message!",
834+
resourceType=resource_type,
835+
resourceId=empty_value,
836+
)
837+
expected_exception_values = {
838+
"resourceType": resource_type,
839+
}
840+
# if the value is None, it is not serialized, even if required
841+
# but if it is an empty string, it will be
842+
if empty_value is not None:
843+
expected_exception_values["resourceId"] = ""
844+
845+
response = _botocore_error_serializer_integration_test(
846+
"verifiedpermissions",
847+
"IsAuthorizedWithToken",
848+
exception,
849+
"ResourceNotFoundException",
850+
404,
851+
"Exception message!",
852+
**expected_exception_values,
853+
)
854+
assert "" not in response
855+
856+
767857
def test_rest_json_protocol_error_serialization_with_additional_members():
768858
class NotFoundException(ServiceException):
769859
code: str = "NotFoundException"
@@ -929,6 +1019,48 @@ class _ResourceNotFoundException(ServiceException):
9291019
assert serialized_response.headers["Smithy-Protocol"] == "rpc-v2-cbor"
9301020

9311021

1022+
@pytest.mark.parametrize("empty_value", ("", None))
1023+
def test_rpc_v2_cbor_protocol_error_serialization_with_empty_required_members(empty_value):
1024+
class AccessDeniedException(ServiceException):
1025+
code: str = "AccessDeniedException"
1026+
sender_fault: bool = True
1027+
status_code: int = 403
1028+
1029+
exception = AccessDeniedException("")
1030+
1031+
_botocore_error_serializer_integration_test(
1032+
"arc-region-switch",
1033+
"ApprovePlanExecutionStep",
1034+
exception,
1035+
"AccessDeniedException",
1036+
403,
1037+
"",
1038+
)
1039+
1040+
1041+
@pytest.mark.parametrize("empty_value", ("", None))
1042+
def test_rpc_v2_cbor_protocol_error_serialization_with_empty_non_required_members(empty_value):
1043+
class _ResourceNotFoundException(ServiceException):
1044+
code: str = "ResourceNotFoundException"
1045+
sender_fault: bool = True
1046+
status_code: int = 404
1047+
ResourceType: str | None
1048+
ResourceId: str | None
1049+
1050+
exception = _ResourceNotFoundException("Not Found", ResourceType="")
1051+
1052+
response = _botocore_error_serializer_integration_test(
1053+
"arc-region-switch",
1054+
"ApprovePlanExecutionStep",
1055+
exception,
1056+
"ResourceNotFoundException",
1057+
404,
1058+
"Not Found",
1059+
protocol="smithy-rpc-v2-cbor",
1060+
)
1061+
assert "ResourceType" not in response
1062+
1063+
9321064
def test_json_protocol_content_type_1_0():
9331065
"""AppRunner defines the jsonVersion 1.0, therefore the Content-Type needs to be application/x-amz-json-1.0."""
9341066
service = load_service("apprunner")

0 commit comments

Comments
 (0)