From ed466f7ef42889cad4409a1538ac4190edd8f604 Mon Sep 17 00:00:00 2001 From: Eldert Grootenboer Date: Wed, 15 Jul 2026 15:04:50 -0700 Subject: [PATCH 1/2] docs: Clarify application_properties returns bytes keys on receive (#45082) - Correct the ServiceBusMessage constructor :paramtype for application_properties from Dict[str, ...] to Dict[Union[str, bytes], ...] to match the existing parameter annotation - Add a note on the application_properties property that, when a message is received, keys and string values are returned as bytes, with the bytes-key access and decode pattern; note the same applies to the raw AMQP annotations and delivery_annotations accessed via raw_amqp_message - Add a README sample for reading application properties from received messages - Add a CHANGELOG Other Changes entry Documentation-only change; no behavioral change. --- sdk/servicebus/azure-servicebus/CHANGELOG.md | 1 + sdk/servicebus/azure-servicebus/README.md | 12 +++++++++++ .../azure/servicebus/_common/message.py | 20 ++++++++++++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/sdk/servicebus/azure-servicebus/CHANGELOG.md b/sdk/servicebus/azure-servicebus/CHANGELOG.md index 0d105a35dc45..b2255f630217 100644 --- a/sdk/servicebus/azure-servicebus/CHANGELOG.md +++ b/sdk/servicebus/azure-servicebus/CHANGELOG.md @@ -16,6 +16,7 @@ ### Other Changes +- Clarified in the `application_properties` documentation (the `ServiceBusMessage` constructor, the `application_properties` property, and the README) that when a message is received, its keys and any string values are returned as `bytes`, not `str`, along with the recommended bytes-key access and decoding pattern. ([#45082](https://github.com/Azure/azure-sdk-for-python/issues/45082)) - When using the async `AmqpOverWebsocket` transport on Python 3.10 or later, `aiohttp>=3.14.0` is now recommended. Earlier `aiohttp` versions have a WebSocket heartbeat bug ([aio-libs/aiohttp#12030](https://github.com/aio-libs/aiohttp/pull/12030)) that can cause the connection to be dropped during long message processing, surfacing as a `SocketError` ("Cannot write to closing transport"). Python 3.9 users must upgrade Python to install an `aiohttp` release containing this fix. ([#44028](https://github.com/Azure/azure-sdk-for-python/issues/44028)) ## 7.14.3 (2025-11-11) diff --git a/sdk/servicebus/azure-servicebus/README.md b/sdk/servicebus/azure-servicebus/README.md index 5488bd3450fd..8a59801d24e5 100644 --- a/sdk/servicebus/azure-servicebus/README.md +++ b/sdk/servicebus/azure-servicebus/README.md @@ -255,6 +255,18 @@ with ServiceBusClient(fully_qualified_namespace, credential) as client: In this example, max_message_count declares the maximum number of messages to attempt receiving before hitting a max_wait_time as specified in seconds. +> **NOTE:** When a message is received, the keys and any string values in `application_properties` are returned as `bytes`, not `str` (for example `{b"order_id": b"12345"}`). Access received properties using bytes keys and decode string values as needed: +> +> ```python +> for message in received_message_array: +> value = message.application_properties.get(b"order_id") +> if isinstance(value, bytes): +> value = value.decode("utf-8") +> print(value) +> ``` +> +> Non-string values (`int`, `bool`, `float`, `uuid.UUID`, `datetime`) are returned as their native types. + > **NOTE:** It should also be noted that `ServiceBusReceiver.peek_messages()` is subtly different than receiving, as it does not lock the messages being peeked, and thus they cannot be settled. diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/_common/message.py b/sdk/servicebus/azure-servicebus/azure/servicebus/_common/message.py index bf177f9c80bb..6e81c54c8115 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/_common/message.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/_common/message.py @@ -66,7 +66,10 @@ class ServiceBusMessage(object): # pylint: disable=too-many-instance-attributes :type body: Optional[Union[str, bytes]] :keyword application_properties: The user defined properties on the message. - :paramtype application_properties: Dict[str, Union[int or float or bool or + Keys may be ``str`` or ``bytes``. Note that when a message is received, the + keys and any string values are returned as ``bytes`` - see the + ``application_properties`` property for details. + :paramtype application_properties: Dict[Union[str, bytes], Union[int or float or bool or bytes or str or uuid.UUID or datetime or None]] :keyword Optional[str] session_id: The session identifier of the message for a sessionful entity. :keyword Optional[str] message_id: The id to identify the message. @@ -289,6 +292,21 @@ def session_id(self, value: Optional[str]) -> None: def application_properties(self) -> Optional[Dict[Union[str, bytes], PrimitiveTypes]]: """The user defined properties on the message. + .. note:: + When a message is received, the keys and any string values in this + dictionary are returned as ``bytes``, not ``str`` (for example + ``{b"order_id": b"12345"}``). Access received properties using bytes + keys, and decode string values as needed:: + + value = message.application_properties.get(b"order_id") + if isinstance(value, bytes): + value = value.decode("utf-8") + + Non-string values (``int``, ``bool``, ``float``, :class:`uuid.UUID`, + ``datetime``) are returned as their native types. The same bytes + behavior applies to the raw AMQP ``annotations`` and + ``delivery_annotations`` accessed through ``raw_amqp_message``. + :rtype: dict[str or bytes, PrimitiveTypes] or None """ return self._raw_amqp_message.application_properties From b42d68d676cc4cc39a40aeee96dc367922f9a2ff Mon Sep 17 00:00:00 2001 From: Eldert Grootenboer Date: Wed, 15 Jul 2026 16:08:55 -0700 Subject: [PATCH 2/2] docs: Address review feedback on application_properties note (#45082) - Guard the Optional application_properties against None before calling .get in both the docstring and README examples, so copying the sample does not raise AttributeError on a message that carries no application properties - Correct the value-type note: an AMQP timestamp decodes to an integer (milliseconds), not a datetime; list only the verified native types (int, bool, float, uuid.UUID) and reframe as an AMQP-decode description --- sdk/servicebus/azure-servicebus/README.md | 7 ++++--- .../azure/servicebus/_common/message.py | 20 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/sdk/servicebus/azure-servicebus/README.md b/sdk/servicebus/azure-servicebus/README.md index 8a59801d24e5..1bceaefff66b 100644 --- a/sdk/servicebus/azure-servicebus/README.md +++ b/sdk/servicebus/azure-servicebus/README.md @@ -255,17 +255,18 @@ with ServiceBusClient(fully_qualified_namespace, credential) as client: In this example, max_message_count declares the maximum number of messages to attempt receiving before hitting a max_wait_time as specified in seconds. -> **NOTE:** When a message is received, the keys and any string values in `application_properties` are returned as `bytes`, not `str` (for example `{b"order_id": b"12345"}`). Access received properties using bytes keys and decode string values as needed: +> **NOTE:** When a message is received, the keys and any string values in `application_properties` are returned as `bytes`, not `str` (for example `{b"order_id": b"12345"}`). The property is `None` when the message has no application properties, so guard for that before indexing. Access received properties using bytes keys and decode string values as needed: > > ```python > for message in received_message_array: -> value = message.application_properties.get(b"order_id") +> props = message.application_properties or {} +> value = props.get(b"order_id") > if isinstance(value, bytes): > value = value.decode("utf-8") > print(value) > ``` > -> Non-string values (`int`, `bool`, `float`, `uuid.UUID`, `datetime`) are returned as their native types. +> Non-string values are returned as decoded by the AMQP layer: `int`, `bool`, `float`, and `uuid.UUID` keep their native types, while an AMQP timestamp is returned as an integer (milliseconds since the Unix epoch), not a `datetime`. > **NOTE:** It should also be noted that `ServiceBusReceiver.peek_messages()` is subtly different than receiving, as it does not lock the messages being peeked, and thus they cannot be settled. diff --git a/sdk/servicebus/azure-servicebus/azure/servicebus/_common/message.py b/sdk/servicebus/azure-servicebus/azure/servicebus/_common/message.py index 6e81c54c8115..5bd5319814d1 100644 --- a/sdk/servicebus/azure-servicebus/azure/servicebus/_common/message.py +++ b/sdk/servicebus/azure-servicebus/azure/servicebus/_common/message.py @@ -295,17 +295,23 @@ def application_properties(self) -> Optional[Dict[Union[str, bytes], PrimitiveTy .. note:: When a message is received, the keys and any string values in this dictionary are returned as ``bytes``, not ``str`` (for example - ``{b"order_id": b"12345"}``). Access received properties using bytes - keys, and decode string values as needed:: + ``{b"order_id": b"12345"}``). The property is ``None`` when the + message carries no application properties, so guard for that before + indexing. Access received properties using bytes keys, and decode + string values as needed:: - value = message.application_properties.get(b"order_id") + props = message.application_properties or {} + value = props.get(b"order_id") if isinstance(value, bytes): value = value.decode("utf-8") - Non-string values (``int``, ``bool``, ``float``, :class:`uuid.UUID`, - ``datetime``) are returned as their native types. The same bytes - behavior applies to the raw AMQP ``annotations`` and - ``delivery_annotations`` accessed through ``raw_amqp_message``. + Non-string values are returned as decoded by the AMQP layer: + ``int``, ``bool``, ``float`` and :class:`uuid.UUID` keep their + native types, while an AMQP timestamp is returned as an integer + (milliseconds since the Unix epoch), not a + :class:`datetime.datetime`. The same bytes behavior applies to the + raw AMQP ``annotations`` and ``delivery_annotations`` accessed + through ``raw_amqp_message``. :rtype: dict[str or bytes, PrimitiveTypes] or None """