Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk/servicebus/azure-servicebus/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions sdk/servicebus/azure-servicebus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,19 @@ 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"}`). 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:
> 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 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.


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -289,6 +292,27 @@ 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"}``). 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::

props = message.application_properties or {}
value = props.get(b"order_id")
if isinstance(value, bytes):
value = value.decode("utf-8")

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
"""
return self._raw_amqp_message.application_properties
Expand Down
Loading