From 0986b525e3784102e8e2926f79ad812d0ec6bfe2 Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Thu, 14 May 2026 09:17:29 -0700 Subject: [PATCH 1/3] Improve logging and HTTP response handling in bot app Add async HTTP response completion for non-invoke activities in TeamsBotApplication. Introduce SendingActivity log method and log activity sends in ConversationClient for better traceability. --- core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs | 6 +++++- core/src/Microsoft.Teams.Core/ConversationClient.cs | 2 ++ core/src/Microsoft.Teams.Core/Log.cs | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs b/core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs index 3536f5eac..866007154 100644 --- a/core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs +++ b/core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs @@ -124,14 +124,18 @@ public TeamsBotApplication( Context defaultContext = new(this, teamsActivity); + HttpContext? httpContext = httpContextAccessor.HttpContext; if (teamsActivity.Type != TeamsActivityType.Invoke) { + if (httpContext is not null) + { + await httpContext.Response.CompleteAsync().ConfigureAwait(false); + } await Router.DispatchAsync(defaultContext, cancellationToken).ConfigureAwait(false); } else // invokes { InvokeResponse invokeResponse = await Router.DispatchWithReturnAsync(defaultContext, cancellationToken).ConfigureAwait(false); - HttpContext? httpContext = httpContextAccessor.HttpContext; if (httpContext is not null && invokeResponse is not null) { httpContext.Response.StatusCode = invokeResponse.Status; diff --git a/core/src/Microsoft.Teams.Core/ConversationClient.cs b/core/src/Microsoft.Teams.Core/ConversationClient.cs index bc8ff5379..ad48514b9 100644 --- a/core/src/Microsoft.Teams.Core/ConversationClient.cs +++ b/core/src/Microsoft.Teams.Core/ConversationClient.cs @@ -77,6 +77,8 @@ public class ConversationClient(HttpClient httpClient, ILogger( HttpMethod.Post, url, diff --git a/core/src/Microsoft.Teams.Core/Log.cs b/core/src/Microsoft.Teams.Core/Log.cs index a6d7e04dc..e63649bb8 100644 --- a/core/src/Microsoft.Teams.Core/Log.cs +++ b/core/src/Microsoft.Teams.Core/Log.cs @@ -45,6 +45,9 @@ internal static partial class Log [LoggerMessage(EventId = 10, Level = LogLevel.Information, Message = "Truncating conversation ID for 'agents' channel to comply with length restrictions.")] public static partial void TruncatingConversationId(this ILogger logger); + [LoggerMessage(EventId = 16, Level = LogLevel.Information, Message = "Sending activity to {Url}")] + public static partial void SendingActivity(this ILogger logger, string url); + [LoggerMessage(EventId = 11, Level = LogLevel.Trace, Message = "Updating activity at {Url}: {Activity}")] public static partial void UpdatingActivity(this ILogger logger, string url, string activity); From 3dd32f8f3787705343a9cfe278696042004a776c Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Thu, 14 May 2026 09:25:38 -0700 Subject: [PATCH 2/3] Make logger call null-safe in ConversationClient Updated logger.SendingActivity to use null-conditional operator, preventing potential NullReferenceException if logger is null. --- core/src/Microsoft.Teams.Core/ConversationClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/Microsoft.Teams.Core/ConversationClient.cs b/core/src/Microsoft.Teams.Core/ConversationClient.cs index ad48514b9..cabc9cd99 100644 --- a/core/src/Microsoft.Teams.Core/ConversationClient.cs +++ b/core/src/Microsoft.Teams.Core/ConversationClient.cs @@ -77,7 +77,7 @@ public class ConversationClient(HttpClient httpClient, ILogger( HttpMethod.Post, From fa5cf0e22edb8cddbf78e10fbc46e80aa2f445e2 Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Thu, 14 May 2026 09:30:01 -0700 Subject: [PATCH 3/3] Make logger call nullable-safe in ConversationClient. When ChannelId is "agents", use "acf" as the conversation ID in outgoing URLs. Add unit tests to verify correct URL formatting and query string handling for agents channel scenarios. --- .../ConversationClient.cs | 2 +- .../ConversationClientTests.cs | 73 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/core/src/Microsoft.Teams.Core/ConversationClient.cs b/core/src/Microsoft.Teams.Core/ConversationClient.cs index cabc9cd99..dbf288c92 100644 --- a/core/src/Microsoft.Teams.Core/ConversationClient.cs +++ b/core/src/Microsoft.Teams.Core/ConversationClient.cs @@ -65,7 +65,7 @@ public class ConversationClient(HttpClient httpClient, ILogger 100 ? conversationId[..100] : conversationId; url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(convId)}/activities/"; } diff --git a/core/test/Microsoft.Teams.Core.UnitTests/ConversationClientTests.cs b/core/test/Microsoft.Teams.Core.UnitTests/ConversationClientTests.cs index f371c25d8..371f71ed4 100644 --- a/core/test/Microsoft.Teams.Core.UnitTests/ConversationClientTests.cs +++ b/core/test/Microsoft.Teams.Core.UnitTests/ConversationClientTests.cs @@ -172,6 +172,79 @@ public async Task SendActivityAsync_ConstructsCorrectUrl() Assert.Equal(HttpMethod.Post, capturedRequest.Method); } + [Fact] + public async Task SendActivityAsync_WithAgentsChannelId_UsesTruncatedConversationId() + { + HttpRequestMessage? capturedRequest = null; + Mock mockHttpMessageHandler = new(); + mockHttpMessageHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((req, ct) => capturedRequest = req) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("{\"id\":\"activity123\"}") + }); + + HttpClient httpClient = new(mockHttpMessageHandler.Object); + ConversationClient conversationClient = new(httpClient); + + CoreActivity activity = new() + { + Type = ActivityType.Message, + ChannelId = "agents", + ServiceUrl = new Uri("https://test.service.url/"), + Conversation = new("conv123") + }; + + await conversationClient.SendActivityAsync(activity); + + Assert.NotNull(capturedRequest); + Assert.Equal("https://test.service.url/v3/conversations/acf/activities/", capturedRequest.RequestUri?.ToString()); + } + + [Fact] + public async Task SendActivityAsync_WithAgentsChannelIdAndIsTargeted_AppendsQueryString() + { + HttpRequestMessage? capturedRequest = null; + Mock mockHttpMessageHandler = new(); + mockHttpMessageHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((req, ct) => capturedRequest = req) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("{\"id\":\"activity123\"}") + }); + + HttpClient httpClient = new(mockHttpMessageHandler.Object); + ConversationClient conversationClient = new(httpClient); + +#pragma warning disable ExperimentalTeamsTargeted + CoreActivity activity = new() + { + Type = ActivityType.Message, + ChannelId = "agents", + ServiceUrl = new Uri("https://test.service.url/"), + Conversation = new("conv123"), + Recipient = new ConversationAccount { IsTargeted = true } + }; +#pragma warning restore ExperimentalTeamsTargeted + + await conversationClient.SendActivityAsync(activity); + + Assert.NotNull(capturedRequest); + Assert.Equal("https://test.service.url/v3/conversations/acf/activities/?isTargetedActivity=true", capturedRequest.RequestUri?.ToString()); + } + [Fact] public async Task SendActivityAsync_WithIsTargeted_AppendsQueryString() {