Skip to content

Commit eeb7626

Browse files
cartertinneyCopilot
andcommitted
fix: reject unsupported message data directly
Keep telemetry size checks as direct get_size comparisons and surface invalid caller-provided payload types as TypeError before starting a pipeline operation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3fddf5c commit eeb7626

7 files changed

Lines changed: 81 additions & 31 deletions

File tree

‎azure-iot-device/azure/iot/device/iothub/aio/async_clients.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,12 +343,13 @@ async def send_message(self, message: Union[Message, str]) -> None:
343343
connected (and there is no auto-connect enabled)
344344
:raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
345345
during execution.
346+
:raises: TypeError if the message data type is not supported by the MQTT transport.
346347
:raises: ValueError if the message fails size validation.
347348
"""
348349
if not isinstance(message, Message):
349350
message = Message(message)
350351

351-
if message._is_size_exceeded(device_constant.TELEMETRY_MESSAGE_SIZE_LIMIT):
352+
if message.get_size() > device_constant.TELEMETRY_MESSAGE_SIZE_LIMIT:
352353
raise ValueError("Size of telemetry message can not exceed 256 KB.")
353354

354355
logger.info("Sending message to Hub...")
@@ -643,14 +644,15 @@ async def send_message_to_output(self, message: Union[Message, str], output_name
643644
connected (and there is no auto-connect enabled)
644645
:raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
645646
during execution.
647+
:raises: TypeError if the message data type is not supported by the MQTT transport.
646648
:raises: ValueError if the message fails size validation.
647649
"""
648650
if not isinstance(message, Message):
649651
message = Message(message)
650652

651653
message.output_name = output_name
652654

653-
if message._is_size_exceeded(device_constant.TELEMETRY_MESSAGE_SIZE_LIMIT):
655+
if message.get_size() > device_constant.TELEMETRY_MESSAGE_SIZE_LIMIT:
654656
raise ValueError("Size of message can not exceed 256 KB.")
655657

656658
logger.info("Sending message to output:" + output_name + "...")

‎azure-iot-device/azure/iot/device/iothub/models/message.py‎

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,14 @@
1010
from azure.iot.device import constant
1111

1212

13-
def _is_message_data_supported(data):
14-
return data is None or isinstance(data, (str, bytes, bytearray, int, float))
15-
16-
1713
def _encode_message_data(data):
1814
if isinstance(data, str):
1915
return data.encode("utf-8")
2016
if isinstance(data, (int, float)):
2117
return str(data).encode("ascii")
2218
if data is None:
2319
return b""
24-
if not _is_message_data_supported(data):
20+
if not isinstance(data, (bytes, bytearray)):
2521
raise TypeError("Message data must be a string, bytes, bytearray, int, float, or None.")
2622
return data
2723

@@ -124,10 +120,6 @@ def set_as_security_message(self):
124120
def __str__(self):
125121
return str(self.data)
126122

127-
def _is_size_exceeded(self, size_limit: int) -> bool:
128-
# Unsupported payloads are deferred to the MQTT transport to preserve client error handling.
129-
return _is_message_data_supported(self.data) and self.get_size() > size_limit
130-
131123
def get_size(self) -> int:
132124
"""Return the message size in bytes as measured by IoT Hub.
133125

‎azure-iot-device/azure/iot/device/iothub/sync_clients.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,12 +338,13 @@ def send_message(self, message: Union[Message, str]) -> None:
338338
connected (and there is no auto-connect enabled)
339339
:raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
340340
during execution.
341+
:raises: TypeError if the message data type is not supported by the MQTT transport.
341342
:raises: ValueError if the message fails size validation.
342343
"""
343344
if not isinstance(message, Message):
344345
message = Message(message)
345346

346-
if message._is_size_exceeded(device_constant.TELEMETRY_MESSAGE_SIZE_LIMIT):
347+
if message.get_size() > device_constant.TELEMETRY_MESSAGE_SIZE_LIMIT:
347348
raise ValueError("Size of telemetry message can not exceed 256 KB.")
348349

349350
logger.info("Sending message to Hub...")
@@ -664,14 +665,15 @@ def send_message_to_output(self, message: Union[Message, str], output_name: str)
664665
connected (and there is no auto-connect enabled)
665666
:raises: :class:`azure.iot.device.exceptions.ClientError` if there is an unexpected failure
666667
during execution.
668+
:raises: TypeError if the message data type is not supported by the MQTT transport.
667669
:raises: ValueError if the message fails size validation.
668670
"""
669671
if not isinstance(message, Message):
670672
message = Message(message)
671673

672674
message.output_name = output_name
673675

674-
if message._is_size_exceeded(device_constant.TELEMETRY_MESSAGE_SIZE_LIMIT):
676+
if message.get_size() > device_constant.TELEMETRY_MESSAGE_SIZE_LIMIT:
675677
raise ValueError("Size of message can not exceed 256 KB.")
676678

677679
logger.info("Sending message to output:" + output_name + "...")

‎tests/e2e/iothub_e2e/aio/test_send_message.py‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import logging
77
import json
88
import dev_utils
9-
from azure.iot.device.exceptions import OperationCancelled, ClientError
9+
from azure.iot.device.exceptions import OperationCancelled
1010

1111
logger = logging.getLogger(__name__)
1212
logger.setLevel(level=logging.INFO)
@@ -53,9 +53,8 @@ async def test_bad_payload_raises(self, client, leak_tracker):
5353
def thing_that_cant_serialize():
5454
pass
5555

56-
with pytest.raises(ClientError) as e_info:
56+
with pytest.raises(TypeError, match="Message data must be"):
5757
await client.send_message(thing_that_cant_serialize)
58-
assert isinstance(e_info.value.__cause__, TypeError)
5958

6059
# TODO: investigate leak
6160
# leak_tracker.check_for_leaks()

‎tests/e2e/iothub_e2e/sync/test_sync_send_message.py‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import json
77
import time
88
import dev_utils
9-
from azure.iot.device.exceptions import OperationCancelled, ClientError
9+
from azure.iot.device.exceptions import OperationCancelled
1010

1111
logger = logging.getLogger(__name__)
1212
logger.setLevel(level=logging.INFO)
@@ -50,9 +50,8 @@ def test_sync_bad_payload_raises(self, client, leak_tracker):
5050
def thing_that_cant_serialize():
5151
pass
5252

53-
with pytest.raises(ClientError) as e_info:
53+
with pytest.raises(TypeError, match="Message data must be"):
5454
client.send_message(thing_that_cant_serialize)
55-
assert isinstance(e_info.value.__cause__, TypeError)
5655

5756
# TODO; investigate this leak
5857
# leak_tracker.check_for_leaks()

‎tests/unit/iothub/aio/test_async_clients.py‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -661,10 +661,7 @@ def fail_send_message(message, callback):
661661
pytest.param(1.5, id="Float input"),
662662
pytest.param(b"message", id="Bytes input"),
663663
pytest.param(bytearray(b"message"), id="Bytearray input"),
664-
pytest.param(object(), id="Object input"),
665664
pytest.param(None, id="None input"),
666-
pytest.param([1, "str"], id="List input"),
667-
pytest.param({"a": 2}, id="Dictionary input"),
668665
],
669666
)
670667
async def test_wraps_data_in_message_and_calls_pipeline_send_message(
@@ -676,6 +673,23 @@ async def test_wraps_data_in_message_and_calls_pipeline_send_message(
676673
assert isinstance(sent_message, Message)
677674
assert sent_message.data == message_input
678675

676+
@pytest.mark.it("Raises TypeError when the message data type is unsupported")
677+
@pytest.mark.parametrize(
678+
"message_input",
679+
[
680+
pytest.param(object(), id="Object input"),
681+
pytest.param([1, "str"], id="List input"),
682+
pytest.param({"a": 2}, id="Dictionary input"),
683+
],
684+
)
685+
async def test_raises_type_error_for_unsupported_message_data(
686+
self, client, mqtt_pipeline, message_input
687+
):
688+
with pytest.raises(TypeError, match="Message data must be"):
689+
await client.send_message(message_input)
690+
691+
assert mqtt_pipeline.send_message.call_count == 0
692+
679693
@pytest.mark.it("Validates message size at the 256 KB boundary")
680694
@pytest.mark.parametrize(
681695
"size_delta, raises_error",
@@ -2049,10 +2063,7 @@ def fail_send_output_message(message, callback):
20492063
pytest.param(1.5, id="Float input"),
20502064
pytest.param(b"message", id="Bytes input"),
20512065
pytest.param(bytearray(b"message"), id="Bytearray input"),
2052-
pytest.param(object(), id="Object input"),
20532066
pytest.param(None, id="None input"),
2054-
pytest.param([1, "str"], id="List input"),
2055-
pytest.param({"a": 2}, id="Dictionary input"),
20562067
],
20572068
)
20582069
async def test_send_message_to_output_calls_pipeline_wraps_data_in_message(
@@ -2065,6 +2076,23 @@ async def test_send_message_to_output_calls_pipeline_wraps_data_in_message(
20652076
assert isinstance(sent_message, Message)
20662077
assert sent_message.data == message_input
20672078

2079+
@pytest.mark.it("Raises TypeError when the message data type is unsupported")
2080+
@pytest.mark.parametrize(
2081+
"message_input",
2082+
[
2083+
pytest.param(object(), id="Object input"),
2084+
pytest.param([1, "str"], id="List input"),
2085+
pytest.param({"a": 2}, id="Dictionary input"),
2086+
],
2087+
)
2088+
async def test_raises_type_error_for_unsupported_message_data(
2089+
self, client, mqtt_pipeline, message_input
2090+
):
2091+
with pytest.raises(TypeError, match="Message data must be"):
2092+
await client.send_message_to_output(message_input, "some_output")
2093+
2094+
assert mqtt_pipeline.send_output_message.call_count == 0
2095+
20682096
@pytest.mark.it("Validates message size at the 256 KB boundary")
20692097
@pytest.mark.parametrize(
20702098
"size_delta, raises_error",

‎tests/unit/iothub/test_sync_clients.py‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -650,10 +650,7 @@ def test_raises_error_on_pipeline_op_error(
650650
pytest.param(1.5, id="Float input"),
651651
pytest.param(b"message", id="Bytes input"),
652652
pytest.param(bytearray(b"message"), id="Bytearray input"),
653-
pytest.param(object(), id="Object input"),
654653
pytest.param(None, id="None input"),
655-
pytest.param([1, "str"], id="List input"),
656-
pytest.param({"a": 2}, id="Dictionary input"),
657654
],
658655
)
659656
def test_wraps_data_in_message_and_calls_pipeline_send_message(
@@ -665,6 +662,23 @@ def test_wraps_data_in_message_and_calls_pipeline_send_message(
665662
assert isinstance(sent_message, Message)
666663
assert sent_message.data == message_input
667664

665+
@pytest.mark.it("Raises TypeError when the message data type is unsupported")
666+
@pytest.mark.parametrize(
667+
"message_input",
668+
[
669+
pytest.param(object(), id="Object input"),
670+
pytest.param([1, "str"], id="List input"),
671+
pytest.param({"a": 2}, id="Dictionary input"),
672+
],
673+
)
674+
def test_raises_type_error_for_unsupported_message_data(
675+
self, client, mqtt_pipeline, message_input
676+
):
677+
with pytest.raises(TypeError, match="Message data must be"):
678+
client.send_message(message_input)
679+
680+
assert mqtt_pipeline.send_message.call_count == 0
681+
668682
@pytest.mark.it("Validates message size at the 256 KB boundary")
669683
@pytest.mark.parametrize(
670684
"size_delta, raises_error",
@@ -2291,10 +2305,7 @@ def test_raises_error_on_pipeline_op_error(
22912305
pytest.param(1.5, id="Float input"),
22922306
pytest.param(b"message", id="Bytes input"),
22932307
pytest.param(bytearray(b"message"), id="Bytearray input"),
2294-
pytest.param(object(), id="Object input"),
22952308
pytest.param(None, id="None input"),
2296-
pytest.param([1, "str"], id="List input"),
2297-
pytest.param({"a": 2}, id="Dictionary input"),
22982309
],
22992310
)
23002311
def test_send_message_to_output_calls_pipeline_wraps_data_in_message(
@@ -2307,6 +2318,23 @@ def test_send_message_to_output_calls_pipeline_wraps_data_in_message(
23072318
assert isinstance(sent_message, Message)
23082319
assert sent_message.data == message_input
23092320

2321+
@pytest.mark.it("Raises TypeError when the message data type is unsupported")
2322+
@pytest.mark.parametrize(
2323+
"message_input",
2324+
[
2325+
pytest.param(object(), id="Object input"),
2326+
pytest.param([1, "str"], id="List input"),
2327+
pytest.param({"a": 2}, id="Dictionary input"),
2328+
],
2329+
)
2330+
def test_raises_type_error_for_unsupported_message_data(
2331+
self, client, mqtt_pipeline, message_input
2332+
):
2333+
with pytest.raises(TypeError, match="Message data must be"):
2334+
client.send_message_to_output(message_input, "some_output")
2335+
2336+
assert mqtt_pipeline.send_output_message.call_count == 0
2337+
23102338
@pytest.mark.it("Validates message size at the 256 KB boundary")
23112339
@pytest.mark.parametrize(
23122340
"size_delta, raises_error",

0 commit comments

Comments
 (0)