-
Notifications
You must be signed in to change notification settings - Fork 403
Fix HttpClient object leak described in #5948 (no socket exhaustion) #5949
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
e7c721a
e5e0676
453a10c
30bec75
d3b8667
f212029
02232cb
2c5fc33
af9d6ca
9e7d2e6
0d737af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Net.Http; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Net.Security; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Security.Cryptography.X509Certificates; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Threading; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| using Microsoft.Identity.Client.Http; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| using Microsoft.Identity.Client.ManagedIdentity; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -24,8 +25,14 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||
| private static readonly ConcurrentDictionary<string, HttpClient> s_httpClientPool = new ConcurrentDictionary<string, HttpClient>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| private static readonly object s_cacheLock = new object(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private static int s_httpClientCreationCount; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // referenced in unit tests | ||||||||||||||||||||||||||||||||||||||||||||||||||
| internal static int HttpClientCreationCount => s_httpClientCreationCount; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private static HttpClient CreateHttpClient() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Interlocked.Increment(ref s_httpClientCreationCount); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| CheckAndManageCache(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| var httpClient = new HttpClient(new HttpClientHandler() | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -63,7 +70,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| public HttpClient GetHttpClient() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return s_httpClientPool.GetOrAdd("non_mtls", CreateHttpClient()); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return s_httpClientPool.GetOrAdd("non_mtls", _ => CreateHttpClient()); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
bgavrilMS marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth a comment here: |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| public HttpClient GetHttpClient(X509Certificate2 x509Certificate2) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -74,7 +81,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| string key = x509Certificate2.Thumbprint; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return s_httpClientPool.GetOrAdd(key, CreateMtlsHttpClient(x509Certificate2)); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return s_httpClientPool.GetOrAdd(key, _ => CreateMtlsHttpClient(x509Certificate2)); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
bgavrilMS marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private static void CheckAndManageCache() | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -88,6 +95,25 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // referenced in unit tests | ||||||||||||||||||||||||||||||||||||||||||||||||||
| internal static void ResetStaticStateForTest() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| lock (s_cacheLock) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach (KeyValuePair<string, HttpClient> pooledClient in s_httpClientPool) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| pooledClient.Value?.Dispose(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| s_httpClientPool.Clear(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Interlocked.Exchange(ref s_httpClientCreationCount, 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+105
to
+117
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| client.Dispose(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| s_httpClientPool.Clear(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Check failure on line 113 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| s_httpClientCreationCount = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Check failure on line 114 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
bgavrilMS marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| client.Dispose(); | |
| } | |
| s_httpClientPool.Clear(); | |
| s_httpClientCreationCount = 0; |
Copilot
AI
Apr 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ResetStaticStateForTest uses KeyValuePair<string, HttpClient> but this file doesn’t import System.Collections.Generic, and the main library project doesn’t enable implicit usings. This should be a compile error; either add the missing using or iterate s_httpClientPool.Values (or use var) to avoid the extra type reference.
| foreach (KeyValuePair<string, HttpClient> pooledClient in s_httpClientPool) | |
| { | |
| pooledClient.Value?.Dispose(); | |
| } | |
| s_httpClientPool.Clear(); | |
| Interlocked.Exchange(ref s_httpClientCreationCount, 0); | |
| } | |
| client.Dispose(); | |
| } | |
| s_httpClientPool.Clear(); | |
| s_httpClientCreationCount = 0; | |
| foreach (HttpClient pooledClient in s_httpClientPool.Values) | |
| { | |
| pooledClient?.Dispose(); | |
| } | |
| s_httpClientPool.Clear(); | |
| Interlocked.Exchange(ref s_httpClientCreationCount, 0); | |
| } |
Check failure on line 143 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs
Azure Pipelines / .NET MSAL PR (YAML) (Linux Integration Tests BuildandRunIntegrationTestsOnLinux)
src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs#L143
src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs(143,1): Error CS1022: Type or namespace definition, or end-of-file expected
Check failure on line 143 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs
Azure Pipelines / .NET MSAL PR (YAML) (Mac Build BuilMacConsoleAppWithBroker)
src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs#L143
src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs(143,1): Error CS1022: Type or namespace definition, or end-of-file expected
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,5 +77,28 @@ public void TestHttpClientWithMtlsCertificateAndCustomHandler() | |
| Assert.AreNotSame(mtlsClient, handlerClient); // Should be different instances | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void TestGetHttpClient_DoesNotLeakHttpClients() | ||
| { | ||
| // Arrange - reset static state so we start from a clean pool | ||
| SimpleHttpClientFactory.ResetStaticStateForTest(); | ||
| var factory = new SimpleHttpClientFactory(); | ||
|
|
||
| // Act - call GetHttpClient multiple times with the same (default) key | ||
| factory.GetHttpClient(); | ||
| factory.GetHttpClient(); | ||
| factory.GetHttpClient(); | ||
|
bgavrilMS marked this conversation as resolved.
|
||
|
|
||
| int created = SimpleHttpClientFactory.HttpClientCreationCount; | ||
|
|
||
| // Assert - CreateHttpClient should be called exactly once. | ||
| // Before the fix, GetOrAdd(key, CreateHttpClient()) eagerly evaluates | ||
| // CreateHttpClient() on every call, causing unnecessary throwaway | ||
| // HttpClient/HttpClientHandler allocations. | ||
| Assert.AreEqual(1, created, | ||
| $"CreateHttpClient was called {created} times for 3 lookups. " + | ||
| "Use GetOrAdd(key, factory_delegate) to avoid creating throwaway HttpClient instances."); | ||
| } | ||
|
|
||
| } | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The regression test only covers GetHttpClient() (the "non_mtls" key). GetHttpClient(X509Certificate2) received the same factory-delegate fix but has no corresponding test. Consider adding a parallel test calling GetHttpClient(cert) multiple times with the same cert and asserting the HttpClientCreationCount only increments once.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in commit |
||
Uh oh!
There was an error while loading. Please reload this page.