Skip to content

Commit ee10a01

Browse files
Tolerate empty structured payloads
This updates http bindings deserializers to tolerate empty payloads when the payload is a collection.
1 parent f55ab14 commit ee10a01

File tree

2 files changed

+89
-10
lines changed

2 files changed

+89
-10
lines changed

packages/smithy-http/src/smithy_http/deserializers.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,44 @@ def read_struct(
8484
)
8585
case Binding.PAYLOAD:
8686
assert binding_matcher.payload_member is not None # noqa: S101
87-
deserializer = self._create_payload_deserializer(
88-
binding_matcher.payload_member
89-
)
90-
consumer(binding_matcher.payload_member, deserializer)
87+
if self._should_read_payload(binding_matcher.payload_member):
88+
deserializer = self._create_payload_deserializer(
89+
binding_matcher.payload_member
90+
)
91+
consumer(binding_matcher.payload_member, deserializer)
9192
case _:
9293
pass
9394

94-
if binding_matcher.has_body:
95+
if binding_matcher.has_body and not self._has_empty_body(
96+
self._response, self._body
97+
):
9598
deserializer = self._create_body_deserializer()
9699
deserializer.read_struct(schema, consumer)
97100

101+
def _should_read_payload(self, schema: Schema) -> bool:
102+
if schema.shape_type not in (
103+
ShapeType.LIST,
104+
ShapeType.MAP,
105+
ShapeType.UNION,
106+
ShapeType.STRUCTURE,
107+
):
108+
return True
109+
return not self._has_empty_body(self._response, self._body)
110+
111+
def _has_empty_body(
112+
self, response: HTTPResponse, body: "SyncStreamingBlob | None"
113+
) -> bool:
114+
if "content-length" in response.fields:
115+
return int(response.fields["content-length"].as_string()) == 0
116+
if isinstance(body, bytes | bytearray):
117+
return len(body) == 0
118+
if (seek := getattr(self._body, "seek", None)) is not None:
119+
content_length = seek(0, 2)
120+
if content_length == 0:
121+
return True
122+
seek(0, 0)
123+
return False
124+
98125
def _create_payload_deserializer(self, payload_member: Schema) -> ShapeDeserializer:
99126
if payload_member.shape_type in (ShapeType.BLOB, ShapeType.STRING):
100127
body = self._body if self._body is not None else self._response.body

packages/smithy-http/tests/unit/test_serializers.py

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ def _consumer(schema: Schema, de: ShapeDeserializer) -> None:
581581

582582
@dataclass
583583
class HTTPStringPayload:
584-
payload: str
584+
payload: str | None = None
585585

586586
ID: ClassVar[ShapeID] = ShapeID("com.smithy#HTTPStringPayload")
587587
SCHEMA: ClassVar[Schema] = Schema.collection(
@@ -594,7 +594,8 @@ def serialize(self, serializer: ShapeSerializer) -> None:
594594
self.serialize_members(s)
595595

596596
def serialize_members(self, serializer: ShapeSerializer) -> None:
597-
serializer.write_string(self.SCHEMA.members["payload"], self.payload)
597+
if self.payload is not None:
598+
serializer.write_string(self.SCHEMA.members["payload"], self.payload)
598599

599600
@classmethod
600601
def deserialize(cls, deserializer: ShapeDeserializer) -> Self:
@@ -684,7 +685,7 @@ def _consumer(schema: Schema, de: ShapeDeserializer) -> None:
684685

685686
@dataclass
686687
class HTTPStructuredPayload:
687-
payload: HTTPStringPayload
688+
payload: HTTPStringPayload | None = None
688689

689690
ID: ClassVar[ShapeID] = ShapeID("com.smithy#HTTPStructuredPayload")
690691
SCHEMA: ClassVar[Schema] = Schema.collection(
@@ -702,7 +703,8 @@ def serialize(self, serializer: ShapeSerializer) -> None:
702703
self.serialize_members(s)
703704

704705
def serialize_members(self, serializer: ShapeSerializer) -> None:
705-
serializer.write_struct(self.SCHEMA.members["payload"], self.payload)
706+
if self.payload is not None:
707+
serializer.write_struct(self.SCHEMA.members["payload"], self.payload)
706708

707709
@classmethod
708710
def deserialize(cls, deserializer: ShapeDeserializer) -> Self:
@@ -1548,6 +1550,53 @@ def payload_cases() -> list[HTTPMessageTestCase]:
15481550
HTTPStructuredPayload(payload=HTTPStringPayload(payload="foo")),
15491551
HTTPMessage(body=BytesIO(b'{"payload":"foo"}')),
15501552
),
1553+
HTTPMessageTestCase(
1554+
HTTPStructuredPayload(HTTPStringPayload()),
1555+
HTTPMessage(body=BytesIO(b"{}")),
1556+
),
1557+
]
1558+
1559+
1560+
class NonSeekableBytesReader:
1561+
def __init__(self, data: bytes) -> None:
1562+
self._wrapped = BytesIO(data)
1563+
1564+
def read(self, size: int = -1, /) -> bytes:
1565+
return self._wrapped.read(size)
1566+
1567+
1568+
def response_payload_cases() -> list[HTTPMessageTestCase]:
1569+
return [
1570+
HTTPMessageTestCase(
1571+
HTTPStructuredPayload(),
1572+
HTTPMessage(body=b""),
1573+
),
1574+
HTTPMessageTestCase(
1575+
HTTPStructuredPayload(),
1576+
HTTPMessage(body=BytesIO(b"")),
1577+
),
1578+
HTTPMessageTestCase(
1579+
HTTPStructuredPayload(),
1580+
HTTPMessage(
1581+
body=NonSeekableBytesReader(b""),
1582+
fields=tuples_to_fields([("content-length", "0")]),
1583+
),
1584+
),
1585+
HTTPMessageTestCase(
1586+
HTTPImplicitPayload(),
1587+
HTTPMessage(body=b""),
1588+
),
1589+
HTTPMessageTestCase(
1590+
HTTPImplicitPayload(),
1591+
HTTPMessage(body=BytesIO(b"")),
1592+
),
1593+
HTTPMessageTestCase(
1594+
HTTPImplicitPayload(),
1595+
HTTPMessage(
1596+
body=NonSeekableBytesReader(b""),
1597+
fields=tuples_to_fields([("content-length", "0")]),
1598+
),
1599+
),
15511600
]
15521601

15531602

@@ -1664,7 +1713,10 @@ async def test_serialize_response_omitting_empty_payload() -> None:
16641713

16651714

16661715
RESPONSE_DESER_CASES: list[HTTPMessageTestCase] = (
1667-
header_cases() + empty_prefix_header_deser_cases() + payload_cases()
1716+
header_cases()
1717+
+ empty_prefix_header_deser_cases()
1718+
+ payload_cases()
1719+
+ response_payload_cases()
16681720
)
16691721

16701722

0 commit comments

Comments
 (0)