Skip to content
Merged
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 @@ -14,6 +14,7 @@
- Fixed a bug where the async receiver factory methods on `azure.servicebus.aio.ServiceBusClient` (`get_queue_receiver`, `get_subscription_receiver`) and the async `ServiceBusReceiver` annotated the `auto_lock_renewer` keyword with the synchronous `AutoLockRenewer`, causing static type checkers to reject the documented `azure.servicebus.aio.AutoLockRenewer` usage. The annotation now references the async `AutoLockRenewer`, matching the docstrings and runtime behavior. ([#47948](https://github.com/Azure/azure-sdk-for-python/issues/47948))
- Fixed a bug where closing a `PEEK_LOCK` receiver did not release messages that had been prefetched into the client buffer or were still in flight, so they remained locked at the broker until lock expiry — delaying their redelivery and inflating their delivery count. On close, a non-session `PEEK_LOCK` receiver now drains the link (stopping the broker and flushing in-flight transfers) and releases the buffered messages (`released` disposition), so the broker can redeliver them immediately without incrementing the delivery count. ([#42917](https://github.com/Azure/azure-sdk-for-python/issues/42917))
- Fixed a bug where the async pure-Python AMQP transport failed to connect with `[Errno 22] Invalid argument` (`amqp:socket-error`) inside containerized/virtualized environments such as Docker Desktop on macOS. The transport no longer reads back and re-applies platform-negotiated TCP options (e.g. `TCP_MAXSEG`) that some platforms reject via `setsockopt`. ([#45394](https://github.com/Azure/azure-sdk-for-python/issues/45394))
- Fixed a bug where passing a `fully_qualified_namespace` that included a port and/or trailing path (for example the `https://<namespace>.servicebus.windows.net:443/` form that Azure returns when provisioning a namespace) raised `ServiceBusAuthenticationError`. The namespace is now normalized to its bare host, matching the .NET and JavaScript SDKs. ([#44034](https://github.com/Azure/azure-sdk-for-python/issues/44034))

### Other Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,10 @@ def __init__(
) -> None:
self._amqp_transport = kwargs.pop("amqp_transport", PyamqpTransport)

# If the user provided http:// or sb://, let's be polite and strip that.
self.fully_qualified_namespace: str = strip_protocol_from_uri(fully_qualified_namespace.strip())
# Keep the port for the non-TLS emulator; strip scheme/port/path otherwise.
self.fully_qualified_namespace: str = strip_protocol_from_uri(
fully_qualified_namespace.strip(), strip_port=kwargs.get("use_tls", True)
)
self._entity_name = entity_name

subscription_name = kwargs.get("subscription_name")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,15 +245,37 @@ def transform_outbound_messages(
return _convert_to_single_service_bus_message(messages, message_type, to_outgoing_amqp_message)


def strip_protocol_from_uri(uri: str) -> str:
"""Removes the protocol (e.g. http:// or sb://) from a URI, such as the FQDN.
def strip_protocol_from_uri(uri: str, *, strip_port: bool = True) -> str:
"""Reduce a URI to its bare host by removing the scheme (e.g. sb://) and path.

The port is also removed by default (e.g. the ``:443/`` in the ARM-emitted
``https://<ns>.servicebus.windows.net:443/``). Pass ``strip_port=False`` for
the development emulator, whose non-default port must be kept.

:param str uri: The URI to modify.
:return: The URI without the protocol.
:keyword bool strip_port: Whether to also remove a trailing port. Defaults to ``True``.
:return: The bare host portion of the URI.
:rtype: str
"""
# Strip the scheme (everything up to and including "//").
left_slash_pos = uri.find("//")
if left_slash_pos != -1:
return uri[left_slash_pos + 2 :]
uri = uri[left_slash_pos + 2 :]
# Strip the path (the first "/" and everything after it).
slash_pos = uri.find("/")
if slash_pos != -1:
uri = uri[:slash_pos]
if not strip_port:
return uri
# Strip the port, but preserve a bracketed IPv6 literal (e.g. "[fe80::1]").
if uri.startswith("["):
bracket_pos = uri.find("]")
if bracket_pos != -1:
uri = uri[: bracket_pos + 1]
else:
colon_pos = uri.find(":")
if colon_pos != -1:
uri = uri[:colon_pos]
return uri


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,10 @@ def __init__(

self._amqp_transport = amqp_transport

# If the user provided http:// or sb://, let's be polite and strip that.
self.fully_qualified_namespace: str = strip_protocol_from_uri(fully_qualified_namespace.strip())
# Keep the port for the non-TLS emulator; strip scheme/port/path otherwise.
self.fully_qualified_namespace: str = strip_protocol_from_uri(
fully_qualified_namespace.strip(), strip_port=kwargs.get("use_tls", True)
)

self._credential = credential
# TODO: can we remove this here? it's recreated in Sender/Receiver
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,10 @@ def __init__(
) -> None:
self._amqp_transport = kwargs.pop("amqp_transport", PyamqpTransportAsync)

# If the user provided http:// or sb://, let's be polite and strip that.
self.fully_qualified_namespace: str = strip_protocol_from_uri(fully_qualified_namespace.strip())
# Keep the port for the non-TLS emulator; strip scheme/port/path otherwise.
self.fully_qualified_namespace: str = strip_protocol_from_uri(
fully_qualified_namespace.strip(), strip_port=kwargs.get("use_tls", True)
)
self._entity_name = entity_name

subscription_name = kwargs.get("subscription_name")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,10 @@ def __init__(
raise ValueError("To use the uAMQP transport, please install `uamqp>=1.6.3,<2.0.0`.") from None

self._amqp_transport = amqp_transport
# If the user provided http:// or sb://, let's be polite and strip that.
self.fully_qualified_namespace: str = strip_protocol_from_uri(fully_qualified_namespace.strip())
# Keep the port for the non-TLS emulator; strip scheme/port/path otherwise.
self.fully_qualified_namespace: str = strip_protocol_from_uri(
fully_qualified_namespace.strip(), strip_port=kwargs.get("use_tls", True)
)
self._credential = credential
self._config = Configuration(
retry_total=retry_total,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,23 @@
import os
import pytest
from azure.servicebus import (
ServiceBusClient,
ServiceBusConnectionStringProperties,
parse_connection_string,
)
from azure.servicebus.aio import ServiceBusClient as ServiceBusClientAsync
from azure.servicebus._common.utils import strip_protocol_from_uri

from devtools_testutils import AzureMgmtRecordedTestCase


class ServiceBusConnectionStringParserTests(AzureMgmtRecordedTestCase):
_EMULATOR_CONN_STR = (
"Endpoint=sb://localhost:5673;SharedAccessKeyName=RootManageSharedAccessKey;"
"SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"
)


class TestServiceBusConnectionStringParser(AzureMgmtRecordedTestCase):
def test_sb_conn_str_parse_cs(self, **kwargs):
conn_str = "Endpoint=sb://resourcename.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX="
parse_result = parse_connection_string(conn_str)
Expand Down Expand Up @@ -103,7 +112,7 @@ def test_sb_parse_malformed_conn_str_lowercase_sa_key_name(self, **kwargs):
parse_result = parse_connection_string(conn_str)
assert str(e.value) == "Connection string must have both SharedAccessKeyName and SharedAccessKey."

def test_sb_parse_malformed_conn_str_lowercase_sa_key_name(self, **kwargs):
def test_sb_parse_malformed_conn_str_lowercase_sa_key(self, **kwargs):
conn_str = "Endpoint=sb://resourcename.servicebus.windows.net/;SharedAccessKeyName=test;sharedaccesskey=THISISATESTKEYXXXXXXXXXXXXXXXXXXXXXXXXXXXX="
with pytest.raises(ValueError) as e:
parse_result = parse_connection_string(conn_str)
Expand All @@ -112,15 +121,45 @@ def test_sb_parse_malformed_conn_str_lowercase_sa_key_name(self, **kwargs):
def test_sb_parse_emulator_string(self, **kwargs):
conn_str = "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"
parse_result = parse_connection_string(conn_str)
assert parse_result.endpoint == "sb://localhost"
assert parse_result.endpoint == "sb://localhost/"
assert parse_result.fully_qualified_namespace == "localhost"

conn_str = "Endpoint=sb://servicebus-emulator;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"
parse_result = parse_connection_string(conn_str)
assert parse_result.endpoint == "sb://servicebus-emulator"
assert parse_result.endpoint == "sb://servicebus-emulator/"
assert parse_result.fully_qualified_namespace == "servicebus-emulator"

conn_str = "Endpoint=sb://192.168.y.z;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"
parse_result = parse_connection_string(conn_str)
assert parse_result.endpoint == "sb://192.168.y.z"
assert parse_result.fully_qualified_namespace == "192.168.y.z"
assert parse_result.endpoint == "sb://192.168.y.z/"
assert parse_result.fully_qualified_namespace == "192.168.y.z"

def test_strip_protocol_from_uri_normalizes_to_bare_host(self, **kwargs):
# regression test for issue #44034: scheme, port and trailing path are
# stripped so the ARM-emitted https://<ns>...:443/ form yields a bare host
host = "myservicebus.servicebus.windows.net"
assert strip_protocol_from_uri(host) == host # bare host (known-good)
assert strip_protocol_from_uri("sb://" + host) == host # protocol only
assert strip_protocol_from_uri("https://" + host) == host
assert strip_protocol_from_uri("http://" + host) == host
assert strip_protocol_from_uri("sb://" + host + "/") == host # trailing slash
assert strip_protocol_from_uri(host + ":443") == host # port only (no scheme)
assert strip_protocol_from_uri(host + ":443/") == host
assert strip_protocol_from_uri("https://" + host + ":443/") == host # ARM/portal-emitted form
assert strip_protocol_from_uri("sb://" + host + ":5671/") == host

# bracketed IPv6 literals are preserved; only a port after "]" is stripped
assert strip_protocol_from_uri("[fe80::1]") == "[fe80::1]"
assert strip_protocol_from_uri("[fe80::1]:5671") == "[fe80::1]"
assert strip_protocol_from_uri("sb://[fe80::1]:5671/") == "[fe80::1]"

def test_emulator_nondefault_port_preserved_sync(self, **kwargs):
client = ServiceBusClient.from_connection_string(_EMULATOR_CONN_STR)
try:
assert client.fully_qualified_namespace == "localhost:5673"
finally:
client.close()

def test_emulator_nondefault_port_preserved_async(self, **kwargs):
client = ServiceBusClientAsync.from_connection_string(_EMULATOR_CONN_STR)
assert client.fully_qualified_namespace == "localhost:5673"
Loading