Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Comment thread
bgavrilMS marked this conversation as resolved.

private static HttpClient CreateHttpClient()
{
Interlocked.Increment(ref s_httpClientCreationCount);
CheckAndManageCache();

var httpClient = new HttpClient(new HttpClientHandler()
Expand Down Expand Up @@ -63,7 +70,7 @@

public HttpClient GetHttpClient()
{
return s_httpClientPool.GetOrAdd("non_mtls", CreateHttpClient());
return s_httpClientPool.GetOrAdd("non_mtls", _ => CreateHttpClient());
Comment thread
bgavrilMS marked this conversation as resolved.
Outdated
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth a comment here: ConcurrentDictionary.GetOrAdd with a factory delegate can still invoke the factory multiple times under contention — multiple threads may each create a competing Lazy<HttpClient> wrapper, but only one is stored. The 'losing' Lazy instances are never .Value-accessed so no HttpClient is constructed for them. The ExecutionAndPublication mode then guarantees only one HttpClient per stored Lazy. These are two separate race scenarios — the concurrent test only validates the second one (LazyThreadSafetyMode), not the first (GetOrAdd factory contention).


public HttpClient GetHttpClient(X509Certificate2 x509Certificate2)
Expand All @@ -74,7 +81,7 @@
}

string key = x509Certificate2.Thumbprint;
return s_httpClientPool.GetOrAdd(key, CreateMtlsHttpClient(x509Certificate2));
return s_httpClientPool.GetOrAdd(key, _ => CreateMtlsHttpClient(x509Certificate2));
Comment thread
bgavrilMS marked this conversation as resolved.
Outdated
}

private static void CheckAndManageCache()
Expand All @@ -88,6 +95,25 @@
}
}

// referenced in unit tests
internal static void ResetStaticStateForTest()
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResetStaticStateForTest and HttpClientCreationCount are test-only hooks in the production assembly with no guard. ResetStaticStateForTest actively disposes and clears production state — accidental non-test usage would be undetected. Is bare internal acceptable here, or should these be behind #if DEBUG / a test-only partial class?

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
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the pool reaches the threshold, CheckAndManageCache clears s_httpClientPool without disposing the created HttpClient instances. Since you already have disposal logic here for tests, consider extracting/reusing it so cache eviction also disposes any Lazy<HttpClient> values that were created, otherwise handlers/sockets can linger until GC/finalization.

Copilot uses AI. Check for mistakes.
client.Dispose();
}
s_httpClientPool.Clear();

Check failure on line 113 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs

View check run for this annotation

Azure Pipelines / .NET MSAL PR (YAML) (Linux Integration Tests BuildandRunIntegrationTestsOnLinux)

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

src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs(113,35): Error CS1519: Invalid token '(' in class, record, struct, or interface member declaration

Check failure on line 113 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs

View check run for this annotation

Azure Pipelines / .NET MSAL PR (YAML) (Linux Integration Tests BuildandRunIntegrationTestsOnLinux)

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

src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs(113,36): Error CS8124: Tuple must contain at least two elements.

Check failure on line 113 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs

View check run for this annotation

Azure Pipelines / .NET MSAL PR (YAML) (Linux Integration Tests BuildandRunIntegrationTestsOnLinux)

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

src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs(113,37): Error CS1519: Invalid token ';' in class, record, struct, or interface member declaration

Check failure on line 113 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs

View check run for this annotation

Azure Pipelines / .NET MSAL PR (YAML) (Mac Build BuilMacConsoleAppWithBroker)

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

src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs(113,35): Error CS1519: Invalid token '(' in class, record, struct, or interface member declaration

Check failure on line 113 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs

View check run for this annotation

Azure Pipelines / .NET MSAL PR (YAML) (Mac Build BuilMacConsoleAppWithBroker)

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

src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs(113,36): Error CS8124: Tuple must contain at least two elements.

Check failure on line 113 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs

View check run for this annotation

Azure Pipelines / .NET MSAL PR (YAML) (Mac Build BuilMacConsoleAppWithBroker)

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

src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs(113,37): Error CS1519: Invalid token ';' in class, record, struct, or interface member declaration

Check failure on line 113 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs

View check run for this annotation

Azure Pipelines / .NET MSAL PR (YAML)

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

src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs(113,35): Error CS1519: Invalid token '(' in class, record, struct, or interface member declaration

Check failure on line 113 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs

View check run for this annotation

Azure Pipelines / .NET MSAL PR (YAML)

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

src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs(113,36): Error CS8124: Tuple must contain at least two elements.

Check failure on line 113 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs

View check run for this annotation

Azure Pipelines / .NET MSAL PR (YAML)

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

src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs(113,37): Error CS1519: Invalid token ';' in class, record, struct, or interface member declaration
s_httpClientCreationCount = 0;

Check failure on line 114 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs

View check run for this annotation

Azure Pipelines / .NET MSAL PR (YAML) (Linux Integration Tests BuildandRunIntegrationTestsOnLinux)

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

src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs(114,39): Error CS1519: Invalid token '=' in class, record, struct, or interface member declaration

Check failure on line 114 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs

View check run for this annotation

Azure Pipelines / .NET MSAL PR (YAML) (Mac Build BuilMacConsoleAppWithBroker)

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

src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs(114,39): Error CS1519: Invalid token '=' in class, record, struct, or interface member declaration

Check failure on line 114 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs

View check run for this annotation

Azure Pipelines / .NET MSAL PR (YAML)

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

src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs(114,39): Error CS1519: Invalid token '=' in class, record, struct, or interface member declaration
Comment thread
bgavrilMS marked this conversation as resolved.
Outdated
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResetStaticStateForTest contains leftover/duplicated code after the lock block (client.Dispose();, extra braces, and an extra s_httpClientPool.Clear() / s_httpClientCreationCount = 0). As written it will not compile (client is undefined) and the braces are unbalanced. Please remove the stray lines and keep the reset logic inside the lock (disposing pooled clients, clearing the pool, and resetting the counter).

Suggested change
client.Dispose();
}
s_httpClientPool.Clear();
s_httpClientCreationCount = 0;

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Apr 27, 2026

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.

Suggested change
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);
}

Copilot uses AI. Check for mistakes.
}
Comment thread
bgavrilMS marked this conversation as resolved.

// This method is used for Service Fabric scenarios where a custom server certificate validation callback is required.
// It allows the caller to provide a custom HttpClientHandler with the callback.
// The server cert rotates so we need a new HttpClient for each call.
Expand All @@ -114,4 +140,4 @@
#endif
}
}
}

Check failure on line 143 in src/client/Microsoft.Identity.Client/PlatformsCommon/Shared/SimpleHttpClientFactory.cs

View check run for this annotation

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

View check run for this annotation

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
Expand Up @@ -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();
Comment thread
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.");
}

}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in commit f212029. TestGetHttpClientWithMtlsCert_DoesNotLeakHttpClients calls GetHttpClient(cert) three times with the same certificate and asserts HttpClientCreationCount == 1. Also added Interlocked.Increment to CreateMtlsHttpClient so the counter tracks mTLS client creation too.

Loading