Skip to content

Commit 330c435

Browse files
committed
Fix HttpClient object leak described in #5984 (no socket exhaustion)
1 parent fdfb876 commit 330c435

2 files changed

Lines changed: 36 additions & 2 deletions

File tree

src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Net.Http;
77
using System.Net.Security;
88
using System.Security.Cryptography.X509Certificates;
9+
using System.Threading;
910
using Microsoft.Identity.Client.Http;
1011
using Microsoft.Identity.Client.ManagedIdentity;
1112

@@ -24,8 +25,12 @@ internal class SimpleHttpClientFactory : IMsalMtlsHttpClientFactory, IMsalSFHttp
2425
private static readonly ConcurrentDictionary<string, HttpClient> s_httpClientPool = new ConcurrentDictionary<string, HttpClient>();
2526
private static readonly object s_cacheLock = new object();
2627

28+
// referenced in unit tests
29+
internal static int HttpClientCreationCount;
30+
2731
private static HttpClient CreateHttpClient()
2832
{
33+
Interlocked.Increment(ref HttpClientCreationCount);
2934
CheckAndManageCache();
3035

3136
var httpClient = new HttpClient(new HttpClientHandler()
@@ -63,7 +68,7 @@ private static HttpClient CreateMtlsHttpClient(X509Certificate2 bindingCertifica
6368

6469
public HttpClient GetHttpClient()
6570
{
66-
return s_httpClientPool.GetOrAdd("non_mtls", CreateHttpClient());
71+
return s_httpClientPool.GetOrAdd("non_mtls", _ => CreateHttpClient());
6772
}
6873

6974
public HttpClient GetHttpClient(X509Certificate2 x509Certificate2)
@@ -74,7 +79,7 @@ public HttpClient GetHttpClient(X509Certificate2 x509Certificate2)
7479
}
7580

7681
string key = x509Certificate2.Thumbprint;
77-
return s_httpClientPool.GetOrAdd(key, CreateMtlsHttpClient(x509Certificate2));
82+
return s_httpClientPool.GetOrAdd(key, _ => CreateMtlsHttpClient(x509Certificate2));
7883
}
7984

8085
private static void CheckAndManageCache()
@@ -88,6 +93,13 @@ private static void CheckAndManageCache()
8893
}
8994
}
9095

96+
// referenced in unit tests
97+
internal static void ResetForTest()
98+
{
99+
s_httpClientPool.Clear();
100+
HttpClientCreationCount = 0;
101+
}
102+
91103
// This method is used for Service Fabric scenarios where a custom server certificate validation callback is required.
92104
// It allows the caller to provide a custom HttpClientHandler with the callback.
93105
// The server cert rotates so we need a new HttpClient for each call.

tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpClientFactoryTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,27 @@ public void TestHttpClientWithMtlsCertificateAndCustomHandler()
7777
Assert.AreNotSame(mtlsClient, handlerClient); // Should be different instances
7878
}
7979

80+
[TestMethod]
81+
public void TestGetHttpClient_DoesNotLeakHttpClients()
82+
{
83+
// Arrange - reset static state so we start from a clean pool
84+
SimpleHttpClientFactory.ResetForTest();
85+
var factory = new SimpleHttpClientFactory();
86+
87+
// Act - call GetHttpClient multiple times with the same (default) key
88+
factory.GetHttpClient();
89+
factory.GetHttpClient();
90+
factory.GetHttpClient();
91+
92+
int created = SimpleHttpClientFactory.HttpClientCreationCount;
93+
94+
// Assert - CreateHttpClient should be called exactly once.
95+
// Before the fix, GetOrAdd(key, CreateHttpClient()) eagerly evaluates
96+
// CreateHttpClient() on every call, leaking HttpClientHandler sockets.
97+
Assert.AreEqual(1, created,
98+
$"CreateHttpClient was called {created} times for 3 lookups. " +
99+
"Use GetOrAdd(key, factory_delegate) to avoid creating throwaway HttpClient instances.");
100+
}
101+
80102
}
81103
}

0 commit comments

Comments
 (0)