diff --git a/changelog.md b/changelog.md index f7ca38055..ff324578e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,14 @@ +# Microsoft 365 Agents SDK for Python - Release Notes v1.9.0 (Unreleased) + +**Release Date:** Unreleased +**Previous Version:** 1.8.0 (Released 2026-10-01) + +## Bug Fixes + +- **Connector User-Agent Headers**: Added the SDK `User-Agent` header to `MCSConnectorClient` and ensured `ConnectorClient`, `TeamsConnectorClient`, and `UserTokenClient` apply it to caller-provided `ClientSession` instances. + +--- + # Microsoft 365 Agents SDK for Python - Release Notes v1.8.0 **Release Date:** 2026-10-01 diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/client/connector_client.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/client/connector_client.py index 0844c578a..be5f73e6e 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/client/connector_client.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/client/connector_client.py @@ -781,18 +781,17 @@ def __init__( if not endpoint.endswith("/"): endpoint += "/" + # Create session with the base URL + session = session or ClientSession(base_url=endpoint) + # Configure headers with JSON acceptance headers = { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": get_product_info(), + "User-Agent": session.headers.get("User-Agent", get_product_info()), } + session.headers.update(headers) - # Create session with the base URL - session = session or ClientSession( - base_url=endpoint, - headers=headers, - ) logger.debug( "ConnectorClient initialized with endpoint: %s and headers: %s", endpoint, diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/client/user_token_client.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/client/user_token_client.py index 0a00543c0..235221d25 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/client/user_token_client.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/client/user_token_client.py @@ -57,18 +57,16 @@ def __init__( if not endpoint.endswith("/"): endpoint += "/" + # Create session with the base URL + session = session or ClientSession(base_url=endpoint) + # Configure headers with JSON acceptance headers = { "Accept": "application/json", "Content-Type": "application/json", - "User-Agent": get_product_info(), + "User-Agent": session.headers.get("User-Agent", get_product_info()), } - - # Create session with the base URL - session = session or ClientSession( - base_url=endpoint, - headers=headers, - ) + session.headers.update(headers) logger.debug( "Creating UserTokenClient with endpoint: %s and headers: %s", endpoint, diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/mcs/mcs_connector_client.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/mcs/mcs_connector_client.py index 707bc5c29..6f50087f3 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/mcs/mcs_connector_client.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/mcs/mcs_connector_client.py @@ -14,6 +14,7 @@ from ..conversations_base import ConversationsBase from ..client._base_client import _BaseClient from .._utils import _handle_request_error +from ..get_product_info import get_product_info logger = logging.getLogger(__name__) @@ -224,6 +225,13 @@ def __init__(self, endpoint: str, client: Optional[ClientSession] = None): self._endpoint = endpoint self._client = client or ClientSession() + headers = { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": self._client.headers.get("User-Agent", get_product_info()), + } + self._client.headers.update(headers) + self._conversations = MCSConversations(self._client, self._endpoint) self._attachments = MCSAttachments() diff --git a/tests/hosting_core/connector/test_connector_client.py b/tests/hosting_core/connector/test_connector_client.py index ab7a03095..19e9fa59a 100644 --- a/tests/hosting_core/connector/test_connector_client.py +++ b/tests/hosting_core/connector/test_connector_client.py @@ -21,6 +21,8 @@ ConnectorClient, ConversationsOperations, ) +from microsoft_agents.hosting.core.connector.get_product_info import get_product_info +from microsoft_agents.hosting.core.connector.teams import TeamsConnectorClient from microsoft_agents.hosting.core.header_propagation import HeaderPropagationContext @@ -945,3 +947,77 @@ async def handler(request): assert "conv_sub_id" in captured["raw_path"] finally: await server.close() + + +class TestUserAgentHeader: + """Ensures ConnectorClient and TeamsConnectorClient send a User-Agent header.""" + + @pytest.mark.asyncio + async def test_connector_client_sets_user_agent_header(self): + client = ConnectorClient(endpoint="https://example.org/", token="token") + try: + assert client.client.headers["User-Agent"] == get_product_info() + finally: + await client.client.close() + + @pytest.mark.asyncio + async def test_connector_client_sends_user_agent_header_on_request(self): + captured = {} + + async def handler(request): + captured["user_agent"] = request.headers.get("User-Agent") + return web.json_response({"id": "activity-id-123"}) + + routes = [web.post("/v3/conversations/{conversation_id}/activities", handler)] + app = _create_app(routes) + + server = TestServer(app) + await server.start_server() + session = ClientSession(base_url=str(server.make_url("/"))) + try: + client = ConnectorClient( + endpoint=str(server.make_url("/")), token="token", session=session + ) + assert client.client is session + await client.conversations.send_to_conversation( + "conv-1", Activity(type="message", text="hi") + ) + finally: + await server.close() + await session.close() + + assert captured["user_agent"] == get_product_info() + + @pytest.mark.asyncio + async def test_teams_connector_client_sets_user_agent_header(self): + client = TeamsConnectorClient(endpoint="https://example.org/", token="token") + try: + assert client.client.headers["User-Agent"] == get_product_info() + finally: + await client.client.close() + + @pytest.mark.asyncio + async def test_teams_connector_client_sends_user_agent_header_on_request(self): + captured = {} + + async def handler(request): + captured["user_agent"] = request.headers.get("User-Agent") + return web.json_response({"id": "team-1"}) + + routes = [web.get("/v3/teams/{team_id}", handler)] + app = _create_app(routes) + + server = TestServer(app) + await server.start_server() + session = ClientSession(base_url=str(server.make_url("/"))) + try: + client = TeamsConnectorClient( + endpoint=str(server.make_url("/")), token="token", session=session + ) + assert client.client is session + await client.fetch_team_details("team-1") + finally: + await server.close() + await session.close() + + assert captured["user_agent"] == get_product_info() diff --git a/tests/hosting_core/connector/test_mcs_connector_client.py b/tests/hosting_core/connector/test_mcs_connector_client.py new file mode 100644 index 000000000..880a23169 --- /dev/null +++ b/tests/hosting_core/connector/test_mcs_connector_client.py @@ -0,0 +1,50 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""Tests for MCSConnectorClient User-Agent header behavior.""" + +import pytest +from aiohttp import web +from aiohttp.test_utils import TestServer + +from microsoft_agents.activity import Activity +from microsoft_agents.hosting.core.connector.get_product_info import get_product_info +from microsoft_agents.hosting.core.connector.mcs import MCSConnectorClient + + +class TestMCSConnectorClientUserAgentHeader: + """Ensures MCSConnectorClient sends a User-Agent header.""" + + @pytest.mark.asyncio + async def test_sets_user_agent_header(self): + client = MCSConnectorClient(endpoint="https://example.org/endpoint") + try: + assert client._client.headers.get("User-Agent") == get_product_info() + finally: + await client.close() + + @pytest.mark.asyncio + async def test_sends_user_agent_header_on_request(self): + captured = {} + + async def handler(request): + captured["user_agent"] = request.headers.get("User-Agent") + return web.json_response({"id": "activity-id-123"}) + + app = web.Application() + app.router.add_post("/endpoint", handler) + server = TestServer(app) + await server.start_server() + try: + endpoint = str(server.make_url("/endpoint")) + client = MCSConnectorClient(endpoint=endpoint) + try: + await client.conversations.send_to_conversation( + "conv-1", Activity(type="message", text="hi") + ) + finally: + await client.close() + finally: + await server.close() + + assert captured["user_agent"] == get_product_info() diff --git a/tests/hosting_core/connector/test_user_token_client.py b/tests/hosting_core/connector/test_user_token_client.py index ff08f7faf..d9ca7513d 100644 --- a/tests/hosting_core/connector/test_user_token_client.py +++ b/tests/hosting_core/connector/test_user_token_client.py @@ -21,6 +21,7 @@ UserToken, UserTokenClient, ) +from microsoft_agents.hosting.core.connector.get_product_info import get_product_info from microsoft_agents.hosting.core.header_propagation import HeaderPropagationContext @@ -407,3 +408,44 @@ async def handler(request): await server.close() assert exc_info.value.status == 500 + + +class TestUserTokenClientUserAgentHeader: + """Ensures UserTokenClient sends a User-Agent header.""" + + @pytest.mark.asyncio + async def test_sets_user_agent_header(self): + client = UserTokenClient("https://example.org/", token="", app_id="app-id") + try: + assert client.client.headers["User-Agent"] == get_product_info() + finally: + await client.close() + + @pytest.mark.asyncio + async def test_sends_user_agent_header_on_request(self): + captured = {} + + async def handler(request): + captured["user_agent"] = request.headers.get("User-Agent") + return web.json_response({"token": "token"}) + + app = web.Application() + app.router.add_get("/api/usertoken/GetToken", handler) + server = TestServer(app) + await server.start_server() + + session = ClientSession(base_url=str(server.make_url("/"))) + try: + client = UserTokenClient( + str(server.make_url("/")), + token="", + app_id="app-id", + session=session, + ) + assert client.client is session + await client.get_user_token("user", "connection", "msteams") + finally: + await session.close() + await server.close() + + assert captured["user_agent"] == get_product_info()