Skip to content

Commit 3ccdfba

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

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
@@ -605,7 +605,7 @@ def _consumer(schema: Schema, de: ShapeDeserializer) -> None:
605605

606606
@dataclass
607607
class HTTPStringPayload:
608-
payload: str
608+
payload: str | None = None
609609

610610
ID: ClassVar[ShapeID] = ShapeID("com.smithy#HTTPStringPayload")
611611
SCHEMA: ClassVar[Schema] = Schema.collection(
@@ -620,7 +620,8 @@ def serialize(self, serializer: ShapeSerializer) -> None:
620620
self.serialize_members(s)
621621

622622
def serialize_members(self, serializer: ShapeSerializer) -> None:
623-
serializer.write_string(self.SCHEMA.members["payload"], self.payload)
623+
if self.payload is not None:
624+
serializer.write_string(self.SCHEMA.members["payload"], self.payload)
624625

625626
@classmethod
626627
def deserialize(cls, deserializer: ShapeDeserializer) -> Self:
@@ -713,7 +714,7 @@ def _consumer(schema: Schema, de: ShapeDeserializer) -> None:
713714

714715
@dataclass
715716
class HTTPStructuredPayload:
716-
payload: HTTPStringPayload
717+
payload: HTTPStringPayload | None = None
717718

718719
ID: ClassVar[ShapeID] = ShapeID("com.smithy#HTTPStructuredPayload")
719720
SCHEMA: ClassVar[Schema] = Schema.collection(
@@ -732,7 +733,8 @@ def serialize(self, serializer: ShapeSerializer) -> None:
732733
self.serialize_members(s)
733734

734735
def serialize_members(self, serializer: ShapeSerializer) -> None:
735-
serializer.write_struct(self.SCHEMA.members["payload"], self.payload)
736+
if self.payload is not None:
737+
serializer.write_struct(self.SCHEMA.members["payload"], self.payload)
736738

737739
@classmethod
738740
def deserialize(cls, deserializer: ShapeDeserializer) -> Self:
@@ -1590,6 +1592,53 @@ def payload_cases() -> list[HTTPMessageTestCase]:
15901592
HTTPStructuredPayload(payload=HTTPStringPayload(payload="foo")),
15911593
HTTPMessage(body=BytesIO(b'{"payload":"foo"}')),
15921594
),
1595+
HTTPMessageTestCase(
1596+
HTTPStructuredPayload(HTTPStringPayload()),
1597+
HTTPMessage(body=BytesIO(b"{}")),
1598+
),
1599+
]
1600+
1601+
1602+
class NonSeekableBytesReader:
1603+
def __init__(self, data: bytes) -> None:
1604+
self._wrapped = BytesIO(data)
1605+
1606+
def read(self, size: int = -1, /) -> bytes:
1607+
return self._wrapped.read(size)
1608+
1609+
1610+
def response_payload_cases() -> list[HTTPMessageTestCase]:
1611+
return [
1612+
HTTPMessageTestCase(
1613+
HTTPStructuredPayload(),
1614+
HTTPMessage(body=b""),
1615+
),
1616+
HTTPMessageTestCase(
1617+
HTTPStructuredPayload(),
1618+
HTTPMessage(body=BytesIO(b"")),
1619+
),
1620+
HTTPMessageTestCase(
1621+
HTTPStructuredPayload(),
1622+
HTTPMessage(
1623+
body=NonSeekableBytesReader(b""),
1624+
fields=tuples_to_fields([("content-length", "0")]),
1625+
),
1626+
),
1627+
HTTPMessageTestCase(
1628+
HTTPImplicitPayload(),
1629+
HTTPMessage(body=b""),
1630+
),
1631+
HTTPMessageTestCase(
1632+
HTTPImplicitPayload(),
1633+
HTTPMessage(body=BytesIO(b"")),
1634+
),
1635+
HTTPMessageTestCase(
1636+
HTTPImplicitPayload(),
1637+
HTTPMessage(
1638+
body=NonSeekableBytesReader(b""),
1639+
fields=tuples_to_fields([("content-length", "0")]),
1640+
),
1641+
),
15931642
]
15941643

15951644

@@ -1706,7 +1755,10 @@ async def test_serialize_response_omitting_empty_payload() -> None:
17061755

17071756

17081757
RESPONSE_DESER_CASES: list[HTTPMessageTestCase] = (
1709-
header_cases() + empty_prefix_header_deser_cases() + payload_cases()
1758+
header_cases()
1759+
+ empty_prefix_header_deser_cases()
1760+
+ payload_cases()
1761+
+ response_payload_cases()
17101762
)
17111763

17121764

0 commit comments

Comments
 (0)