Skip to content

Commit 54fb49e

Browse files
committed
fix: AuthenticationClient: use a shared static HttpClient instead of one per instance
1 parent 2affc63 commit 54fb49e

3 files changed

Lines changed: 41 additions & 14 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
### 2026-10-06 v6.0.1
44

55
* fix: ForceClient, JsonClient and AuthenticationClient no longer dispose an HttpClient passed in by the caller - the caller owns it
6-
- AuthenticationClient still disposes the HttpClient it creates when none is passed
7-
- ForceClient now disposes the AuthenticationClient it uses internally to log in
6+
* fix: AuthenticationClient now uses a shared static HttpClient by default, like ForceClient and JsonClient, instead of creating a new HttpClient (and connection pool) per instance - repeated logins no longer leak sockets
87

98
### 2026-10-06 v6.0.0
109

‎src/NetCoreForce.Client.Tests/DisposeTests.cs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Net;
33
using System.Net.Http;
4+
using System.Reflection;
45
using System.Threading.Tasks;
56
using NetCoreForce.Client;
67
using Xunit;
@@ -47,6 +48,32 @@ public async Task AuthenticationClientDisposeLeavesCallerHttpClientUsable()
4748
await AssertHttpClientUsable(httpClient);
4849
}
4950

51+
[Fact]
52+
public void AuthenticationClientDefaultUsesSharedHttpClient()
53+
{
54+
// a new HttpClient per instance means a new connection pool per login
55+
AuthenticationClient client1 = new AuthenticationClient();
56+
AuthenticationClient client2 = new AuthenticationClient();
57+
58+
HttpClient httpClient1 = GetHttpClient(client1);
59+
HttpClient httpClient2 = GetHttpClient(client2);
60+
61+
Assert.Same(httpClient1, httpClient2);
62+
63+
client1.Dispose();
64+
client2.Dispose();
65+
66+
// throws ObjectDisposedException if the shared HttpClient was disposed
67+
httpClient1.CancelPendingRequests();
68+
}
69+
70+
private static HttpClient GetHttpClient(AuthenticationClient client)
71+
{
72+
FieldInfo field = typeof(AuthenticationClient).GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance);
73+
74+
return (HttpClient)field.GetValue(client);
75+
}
76+
5077
private static HttpClient CreateHttpClient()
5178
{
5279
var mockHandler = new MockHttpClientHandler();

‎src/NetCoreForce.Client/AuthenticationClient.cs‎

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,17 @@ public class AuthenticationClient : IDisposable
2323
private const string UserAgent = "netcoreforce-client";
2424
private const string IntrospectTokenEndpointUrl = "https://login.salesforce.com/services/oauth2/introspect";
2525
private const string TokenRequestEndpointUrl = "https://login.salesforce.com/services/oauth2/token";
26+
//best practice is to reuse HttpClient - a new instance per login would mean a new connection pool per login
27+
private static readonly HttpClient _SharedHttpClient;
2628
private readonly HttpClient _httpClient;
27-
private readonly bool _disposeHttpClient;
29+
30+
static AuthenticationClient()
31+
{
32+
_SharedHttpClient = HttpClientFactory.CreateHttpClient();
33+
}
2834

2935
/// <summary>
30-
/// Initialize the AuthenticationClient with the libary's default Salesforce API version, and default HttpClient
36+
/// Initialize the AuthenticationClient with the libary's default Salesforce API version, and the default shared static HttpClient
3137
/// <para>See the DefaultApiVersion property</para>
3238
/// </summary>
3339
public AuthenticationClient() : this(null, null) { }
@@ -36,7 +42,7 @@ public AuthenticationClient() : this(null, null) { }
3642
/// Initialize the AuthenticationClient with the specified Salesforce API version and/or HttpClient
3743
/// </summary>
3844
/// <param name="apiVersion">Target Salesforce API version</param>
39-
/// <param name="httpClient">Custom HttpClient (Optional). Not disposed by this client; the caller retains ownership.</param>
45+
/// <param name="httpClient">Optional HttpClient object. Defaults to a shared static instance for best performance. Not disposed by this client; the caller retains ownership.</param>
4046
public AuthenticationClient(string apiVersion = null, HttpClient httpClient = null)
4147
{
4248
if (!string.IsNullOrEmpty(apiVersion))
@@ -48,9 +54,8 @@ public AuthenticationClient(string apiVersion = null, HttpClient httpClient = nu
4854
ApiVersion = DefaultApiVersion;
4955
}
5056

51-
//only dispose the HttpClient if this client created it
52-
_disposeHttpClient = httpClient == null;
53-
_httpClient = httpClient ?? new HttpClient();
57+
//use the instance client when supplied, otherwise use the default shared instance.
58+
_httpClient = httpClient ?? _SharedHttpClient;
5459
}
5560

5661
/// <summary>
@@ -369,15 +374,11 @@ public async Task ClientCredentialsAsync(string clientId, string clientSecret,st
369374
}
370375

371376
/// <summary>
372-
/// Dispose client - only disposes the HttpClient if this client created it.
373-
/// <para>A caller-supplied HttpClient is owned by the caller, and is left as-is.</para>
377+
/// Dispose client - does not dispose the HttpClient.
378+
/// <para>A caller-supplied HttpClient is owned by the caller, and the shared static HttpClient is left as-is.</para>
374379
/// </summary>
375380
public void Dispose()
376381
{
377-
if (_disposeHttpClient)
378-
{
379-
_httpClient.Dispose();
380-
}
381382
}
382383
}
383384
}

0 commit comments

Comments
 (0)