From 54fbd57d38ca1f45d4e54b14b50bb18e51ce13fd Mon Sep 17 00:00:00 2001 From: Teddy Arida-Moody Date: Mon, 9 Nov 2026 18:00:01 -0800 Subject: [PATCH] fix: remove Socket Mode URL diagnostic and reject case-variant geos Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Microsoft.Teams.Apps/README.md | 3 +-- src/Microsoft.Teams.Apps/SocketMode/SocketModeObserver.cs | 7 ------- src/Microsoft.Teams.Apps/SocketMode/SocketModeOptions.cs | 1 + src/Microsoft.Teams.Apps/SocketMode/SocketModeTransport.cs | 5 +---- .../SocketMode/SocketModeHostingTests.cs | 1 - .../SocketMode/SocketModeObserverTests.cs | 1 - .../SocketMode/SocketModeTransportTests.cs | 3 ++- 7 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.Teams.Apps/README.md b/src/Microsoft.Teams.Apps/README.md index 48a83e7e..98134424 100644 --- a/src/Microsoft.Teams.Apps/README.md +++ b/src/Microsoft.Teams.Apps/README.md @@ -239,8 +239,7 @@ from host startup; normal shutdown does not raise a disconnect event. These events are experimental and observational. Handlers run synchronously, may run concurrently across geos, and should be short and thread-safe. Observer failures are logged without interrupting delivery or other observers. `SocketMode` is null when Socket Mode is disabled. Read-only diagnostics are available through -`SocketMode.Status` (aggregate), `SocketMode.GeoStatuses` (a per-geo snapshot), `SocketMode.GeoList`, and -`SocketMode.NegotiateUrl` (the resolved endpoint for the first geo). +`SocketMode.Status` (aggregate), `SocketMode.GeoStatuses` (a per-geo snapshot), and `SocketMode.GeoList`. Negotiation failures expose `SocketModeNegotiateException.StatusCode`, `.RetryAfter` (a nullable `TimeSpan`), and `.IsAuthError` for HTTP 401/403. Downstream SignalR negotiation errors are not classified as terminal service-negotiation rejections. diff --git a/src/Microsoft.Teams.Apps/SocketMode/SocketModeObserver.cs b/src/Microsoft.Teams.Apps/SocketMode/SocketModeObserver.cs index 1e664210..a047fc00 100644 --- a/src/Microsoft.Teams.Apps/SocketMode/SocketModeObserver.cs +++ b/src/Microsoft.Teams.Apps/SocketMode/SocketModeObserver.cs @@ -35,11 +35,4 @@ internal SocketModeObserver(Func getTransport, SocketModeEv /// Gets the resolved geo identifiers the transport connects to. public IReadOnlyList GeoList => _transport.Value.GeoList; - - /// - /// Gets the resolved negotiation endpoint for the first configured geo, for diagnostics. - /// Each geo connects to its own endpoint. - /// - /// This URL is managed by the SDK and is for diagnostics only. Do not depend on it; it may change in the future. - public Uri NegotiateUrl => _transport.Value.NegotiateUrl; } diff --git a/src/Microsoft.Teams.Apps/SocketMode/SocketModeOptions.cs b/src/Microsoft.Teams.Apps/SocketMode/SocketModeOptions.cs index 0b69e41e..99d43d34 100644 --- a/src/Microsoft.Teams.Apps/SocketMode/SocketModeOptions.cs +++ b/src/Microsoft.Teams.Apps/SocketMode/SocketModeOptions.cs @@ -24,6 +24,7 @@ public sealed class SocketModeOptions /// Gets or sets the geos to connect, one connection each. /// Defaults to amer, emea, and apac, including when the list is empty. /// Override this to connect to a different set of geos. + /// Entries must be non-empty and unique, ignoring case. /// public IReadOnlyList Geos { get; set; } = SocketModeProtocol.DefaultGeos; diff --git a/src/Microsoft.Teams.Apps/SocketMode/SocketModeTransport.cs b/src/Microsoft.Teams.Apps/SocketMode/SocketModeTransport.cs index bcf0b5d9..df2c13ad 100644 --- a/src/Microsoft.Teams.Apps/SocketMode/SocketModeTransport.cs +++ b/src/Microsoft.Teams.Apps/SocketMode/SocketModeTransport.cs @@ -183,9 +183,6 @@ internal IReadOnlyDictionary GeoStatuses /// Gets a snapshot of the resolved geo identifiers. internal IReadOnlyList GeoList => Array.AsReadOnly(_geos.Select(geo => geo.Geo).ToArray()); - /// Gets the resolved negotiation endpoint for the first geo. - internal Uri NegotiateUrl => _geos[0].NegotiateUri; - /// TimeSpan IGeoSocketOwner.StartupTimeout => _options.StartupTimeout; @@ -513,7 +510,7 @@ private static bool IsInvoke(SocketActivityEnvelope envelope, CoreActivity activ throw new ArgumentException("Socket Mode geos must not contain an empty string.", nameof(geos)); } - if (new HashSet(geos, StringComparer.Ordinal).Count != geos.Count) + if (new HashSet(geos, StringComparer.OrdinalIgnoreCase).Count != geos.Count) { throw new ArgumentException("Socket Mode geos must not contain duplicates.", nameof(geos)); } diff --git a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeHostingTests.cs b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeHostingTests.cs index 8168ae5b..d8aa9d62 100644 --- a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeHostingTests.cs +++ b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeHostingTests.cs @@ -167,7 +167,6 @@ public async Task HostedService_StartsEveryGeoDispatchesToTheAppAndStops() SocketModeTransport transport = provider.GetRequiredService(); Assert.Equal(SocketModeStatus.Idle, socket.Status); Assert.Equal(["amer", "emea"], socket.GeoList); - Assert.Equal(new Uri("https://amer.botapi.skype.com/v3/websockets/connect"), socket.NegotiateUrl); Assert.Empty(socket.GeoStatuses); await hosted.StartAsync(CancellationToken.None); diff --git a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeObserverTests.cs b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeObserverTests.cs index 4549517e..3d053ad3 100644 --- a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeObserverTests.cs +++ b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeObserverTests.cs @@ -31,7 +31,6 @@ public async Task Diagnostics_AreLazyAndDoNotConnect() observer.Events.Ready += (_, _) => { }; Assert.Equal(0, created); Assert.Equal(SocketModeStatus.Idle, observer.Status); - Assert.Equal(new Uri("https://emea.botapi.skype.com/v3/websockets/connect"), observer.NegotiateUrl); Assert.Equal(["emea"], observer.GeoList); Assert.Empty(observer.GeoStatuses); Assert.Equal(1, created); diff --git a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeTransportTests.cs b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeTransportTests.cs index 8366da10..0d868ba9 100644 --- a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeTransportTests.cs +++ b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeTransportTests.cs @@ -50,7 +50,7 @@ public async Task StartAsync_ConnectsEveryDefaultGeo(bool emptyGeos) [Theory] [InlineData(new object[] { new[] { "emea" } })] [InlineData(new object[] { new[] { "custom-geo", "other-geo" } })] - [InlineData(new object[] { new[] { "amer", "AMER" } })] + [InlineData(new object[] { new[] { "AMER" } })] [InlineData(new object[] { new[] { "amer.amer" } })] public async Task StartAsync_ConnectsConfiguredGeos(string[] geos) { @@ -76,6 +76,7 @@ public async Task StartAsync_ConnectsConfiguredGeos(string[] geos) [InlineData(new object[] { new[] { "" } })] [InlineData(new object[] { new[] { "amer", "" } })] [InlineData(new object[] { new[] { "amer", "amer" } })] + [InlineData(new object[] { new[] { "amer", "AMER" } })] public void Constructor_RejectsInvalidGeos(string[]? geos) { Assert.Throws(() => new Harness(new SocketModeTransportOptions { Geos = geos! }).Transport);