Skip to content

Commit 196378f

Browse files
authored
Merge pull request #14 from mjcheetham/fix-httpclient
Don't double dispose HttpClients and ensure all objects are correctly disposed on exit
2 parents bf20c20 + d814f1f commit 196378f

File tree

15 files changed

+227
-101
lines changed

15 files changed

+227
-101
lines changed

src/Microsoft.AzureRepos/AzureDevOpsRestApi.cs

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,14 @@
1212

1313
namespace Microsoft.AzureRepos
1414
{
15-
public interface IAzureDevOpsRestApi
15+
public interface IAzureDevOpsRestApi : IDisposable
1616
{
1717
Task<string> GetAuthorityAsync(Uri organizationUri);
1818
Task<string> CreatePersonalAccessTokenAsync(Uri organizationUri, string accessToken, IEnumerable<string> scopes);
1919
}
2020

2121
public class AzureDevOpsRestApi : IAzureDevOpsRestApi
2222
{
23-
2423
private readonly ICommandContext _context;
2524
private readonly IHttpClientFactory _httpFactory;
2625

@@ -44,11 +43,8 @@ public async Task<string> GetAuthorityAsync(Uri organizationUri)
4443
const string commonAuthority = authorityBase + "common";
4544
const string msaAuthority = authorityBase + "live.com";
4645

47-
var headers = new[] {Constants.Http.AcceptHeader(Constants.Http.MimeTypeJson)};
48-
4946
_context.Trace.WriteLine($"HTTP: HEAD {organizationUri}");
50-
using (var client = _httpFactory.GetClient())
51-
using (var response = await client.SendAsync(HttpMethod.Head, organizationUri, headers))
47+
using (HttpResponseMessage response = await HttpClient.HeadAsync(organizationUri))
5248
{
5349
_context.Trace.WriteLine("HTTP: Response code ignored.");
5450
_context.Trace.WriteLine("Inspecting headers...");
@@ -112,16 +108,10 @@ public async Task<string> CreatePersonalAccessTokenAsync(Uri organizationUri, st
112108

113109
Uri requestUri = new Uri(identityServiceUri, sessionTokenUrl);
114110

115-
var headers = new[]
116-
{
117-
Constants.Http.AcceptHeader(Constants.Http.MimeTypeJson),
118-
Constants.Http.AuthorizationBearerHeader(accessToken)
119-
};
120-
121111
_context.Trace.WriteLine($"HTTP: POST {requestUri}");
122112
using (StringContent content = CreateAccessTokenRequestJson(organizationUri, scopes))
123-
using (var client = _httpFactory.GetClient())
124-
using (var response = await client.SendAsync(HttpMethod.Post, requestUri, headers, content))
113+
using (HttpRequestMessage request = CreateRequestMessage(HttpMethod.Post, requestUri, content, accessToken))
114+
using (HttpResponseMessage response = await HttpClient.SendAsync(request))
125115
{
126116
_context.Trace.WriteLine($"HTTP: Response {(int)response.StatusCode} [{response.StatusCode}]");
127117

@@ -162,15 +152,9 @@ private async Task<Uri> GetIdentityServiceUriAsync(Uri organizationUri, string a
162152
Query = locationServiceQuery,
163153
}.Uri;
164154

165-
var headers = new[]
166-
{
167-
Constants.Http.AcceptHeader(Constants.Http.MimeTypeJson),
168-
Constants.Http.AuthorizationBearerHeader(accessToken)
169-
};
170-
171155
_context.Trace.WriteLine($"HTTP: GET {requestUri}");
172-
using (var client = _httpFactory.GetClient())
173-
using (var response = await client.SendAsync(HttpMethod.Get, requestUri, headers))
156+
using (HttpRequestMessage request = CreateRequestMessage(HttpMethod.Get, requestUri, bearerToken: accessToken))
157+
using (HttpResponseMessage response = await HttpClient.SendAsync(request))
174158
{
175159
_context.Trace.WriteLine($"HTTP: Response {(int)response.StatusCode} [{response.StatusCode}]");
176160
if (response.IsSuccessStatusCode)
@@ -194,6 +178,24 @@ private async Task<Uri> GetIdentityServiceUriAsync(Uri organizationUri, string a
194178

195179
private const RegexOptions CommonRegexOptions = RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase;
196180

181+
private HttpClient _httpClient;
182+
183+
private HttpClient HttpClient
184+
{
185+
get
186+
{
187+
if (_httpClient is null)
188+
{
189+
_httpClient = _httpFactory.CreateClient();
190+
191+
// Configure the HTTP client with standard headers for Azure Repos API calls
192+
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(Constants.Http.MimeTypeJson));
193+
}
194+
195+
return _httpClient;
196+
}
197+
}
198+
197199
/// <summary>
198200
/// Attempt to extract the authority from a Authorization Bearer header.
199201
/// </summary>
@@ -270,6 +272,40 @@ private static StringContent CreateAccessTokenRequestJson(Uri organizationUri, I
270272
return content;
271273
}
272274

275+
/// <summary>
276+
/// Create an <see cref="HttpRequestMessage"/> with optional content and bearer-token authorization header.
277+
/// </summary>
278+
/// <param name="method">HTTP request method type.</param>
279+
/// <param name="uri">Request URI.</param>
280+
/// <param name="content">Optional request content.</param>
281+
/// <param name="bearerToken">Optional bearer token for authorization.</param>
282+
/// <returns>HTTP request message.</returns>
283+
private static HttpRequestMessage CreateRequestMessage(HttpMethod method, Uri uri, HttpContent content = null, string bearerToken = null)
284+
{
285+
var request = new HttpRequestMessage(method, uri);
286+
287+
if (!(content is null))
288+
{
289+
request.Content = content;
290+
}
291+
292+
if (!string.IsNullOrWhiteSpace(bearerToken))
293+
{
294+
request.Headers.Authorization = new AuthenticationHeaderValue(Constants.Http.WwwAuthenticateBearerScheme, bearerToken);
295+
}
296+
297+
return request;
298+
}
299+
300+
#endregion
301+
302+
#region IDisposable
303+
304+
public void Dispose()
305+
{
306+
_httpClient?.Dispose();
307+
}
308+
273309
#endregion
274310
}
275311
}

src/Microsoft.AzureRepos/AzureReposHostProvider.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ public override async Task<GitCredential> CreateCredentialAsync(InputArguments i
8585
return new GitCredential(Constants.PersonalAccessTokenUserName, pat);
8686
}
8787

88+
protected override void Dispose(bool disposing)
89+
{
90+
if (disposing)
91+
{
92+
_azDevOps.Dispose();
93+
}
94+
95+
base.Dispose(disposing);
96+
}
97+
8898
#endregion
8999
}
90100
}

src/Microsoft.Git.CredentialManager/Authentication/WindowsIntegratedAuthentication.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
namespace Microsoft.Git.CredentialManager.Authentication
99
{
10-
public interface IWindowsIntegratedAuthentication
10+
public interface IWindowsIntegratedAuthentication : IDisposable
1111
{
1212
Task<bool> GetIsSupportedAsync(Uri uri);
1313
}
@@ -33,8 +33,7 @@ public async Task<bool> GetIsSupportedAsync(Uri uri)
3333
bool supported = false;
3434

3535
_context.Trace.WriteLine($"HTTP: HEAD {uri}");
36-
using (HttpClient client = _httpFactory.GetClient())
37-
using (HttpResponseMessage response = await client.SendAsync(HttpMethod.Head, uri))
36+
using (HttpResponseMessage response = await HttpClient.HeadAsync(uri))
3837
{
3938
_context.Trace.WriteLine("HTTP: Response code ignored.");
4039

@@ -56,5 +55,17 @@ public async Task<bool> GetIsSupportedAsync(Uri uri)
5655

5756
return supported;
5857
}
58+
59+
private HttpClient _httpClient;
60+
private HttpClient HttpClient => _httpClient ?? (_httpClient = _httpFactory.CreateClient());
61+
62+
#region IDisposable
63+
64+
public void Dispose()
65+
{
66+
_httpClient?.Dispose();
67+
}
68+
69+
#endregion
5970
}
6071
}

src/Microsoft.Git.CredentialManager/Constants.cs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,12 @@ public static class EnvironmentVariables
2020

2121
public static class Http
2222
{
23+
public const string WwwAuthenticateBasicScheme = "Basic";
2324
public const string WwwAuthenticateBearerScheme = "Bearer";
2425
public const string WwwAuthenticateNegotiateScheme = "Negotiate";
2526
public const string WwwAuthenticateNtlmScheme = "NTLM";
2627

2728
public const string MimeTypeJson = "application/json";
28-
29-
public static Header AcceptHeader(string value)
30-
{
31-
return new Header("Accept", new[] {value});
32-
}
33-
34-
public static Header AuthorizationBearerHeader(string bearerToken)
35-
{
36-
return new Header("Authorization", new[] {$"{WwwAuthenticateBearerScheme} {bearerToken}"});
37-
}
3829
}
3930

4031
/// <summary>

src/Microsoft.Git.CredentialManager/GenericHostProvider.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ public override async Task<GitCredential> CreateCredentialAsync(InputArguments i
7171
return _basicAuth.GetCredentials(uri.AbsoluteUri, uri.UserInfo);
7272
}
7373

74+
protected override void Dispose(bool disposing)
75+
{
76+
_winAuth.Dispose();
77+
base.Dispose(disposing);
78+
}
79+
7480
#endregion
7581

7682
#region Helpers

src/Microsoft.Git.CredentialManager/HostProvider.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
3+
4+
using System;
35
using System.Threading.Tasks;
46

57
namespace Microsoft.Git.CredentialManager
68
{
79
/// <summary>
810
/// Represents a particular Git hosting service and provides for the creation of credentials to access the remote.
911
/// </summary>
10-
public interface IHostProvider
12+
public interface IHostProvider : IDisposable
1113
{
1214
/// <summary>
1315
/// Name of the hosting provider.
@@ -60,5 +62,22 @@ protected HostProvider(ICommandContext context)
6062
public abstract string GetCredentialKey(InputArguments input);
6163

6264
public abstract Task<GitCredential> CreateCredentialAsync(InputArguments input);
65+
66+
/// <summary>
67+
/// Called when the application is being terminated. Clean up and release any resources.
68+
/// </summary>
69+
/// <param name="disposing">True if the instance is being disposed, false if being finalized.</param>
70+
protected virtual void Dispose(bool disposing) { }
71+
72+
public void Dispose()
73+
{
74+
Dispose(true);
75+
GC.SuppressFinalize(this);
76+
}
77+
78+
~HostProvider()
79+
{
80+
Dispose(false);
81+
}
6382
}
6483
}

src/Microsoft.Git.CredentialManager/HostProviderRegistry.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ namespace Microsoft.Git.CredentialManager
1010
/// Represents a collection of <see cref="IHostProvider"/>s which are selected based on Git credential query
1111
/// <see cref="InputArguments"/>.
1212
/// </summary>
13-
public interface IHostProviderRegistry
13+
/// <remarks>
14+
/// All registered <see cref="IHostProvider"/>s will be disposed when this <see cref="IHostProviderRegistry"/> is disposed.
15+
/// </remarks>
16+
public interface IHostProviderRegistry : IDisposable
1417
{
1518
/// <summary>
1619
/// Add the given <see cref="IHostProvider"/>(s) to this registry.
1720
/// </summary>
1821
/// <param name="hostProviders">A collection of providers to register.</param>
22+
/// <remarks>Providers will be disposed of when this registry instance is disposed itself.</remarks>
1923
void Register(params IHostProvider[] hostProviders);
2024

2125
/// <summary>
@@ -56,5 +60,14 @@ public IHostProvider GetProvider(InputArguments input)
5660

5761
return provider;
5862
}
63+
64+
public void Dispose()
65+
{
66+
// Dispose of all registered providers to give them a chance to clean up and release any resources
67+
foreach (IHostProvider provider in _hostProviders)
68+
{
69+
provider.Dispose();
70+
}
71+
}
5972
}
6073
}

src/Microsoft.Git.CredentialManager/HttpClientFactory.cs

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,40 @@
66
namespace Microsoft.Git.CredentialManager
77
{
88
/// <summary>
9-
/// Acquires <see cref="HttpClient"/>s that have been configured for use in Git Credential Manager.
9+
/// Constructs <see cref="HttpClient"/>s that have been configured for use in Git Credential Manager.
1010
/// </summary>
1111
public interface IHttpClientFactory
1212
{
1313
/// <summary>
14-
/// Get an instance of <see cref="HttpClient"/> with default request headers set.
14+
/// Get a new instance of <see cref="HttpClient"/> with default request headers set.
1515
/// </summary>
16-
/// <returns>Client with default headers.</returns>
17-
HttpClient GetClient();
16+
/// <remarks>
17+
/// Callers should reuse instances of <see cref="HttpClient"/> returned from this method as long
18+
/// as they are needed, rather than repeatably call this method to create new ones.
19+
/// <para/>
20+
/// Creating a new <see cref="HttpClient"/> consumes one free socket which may not be released
21+
/// by the Operating System until sometime after the client is disposed, leading to possible free
22+
/// socket exhaustion.
23+
/// </remarks>
24+
/// <returns>New client instance with default headers.</returns>
25+
HttpClient CreateClient();
1826
}
1927

2028
public class HttpClientFactory : IHttpClientFactory
2129
{
22-
private HttpClient _client;
23-
24-
public HttpClient GetClient()
30+
public HttpClient CreateClient()
2531
{
26-
if (_client is null)
27-
{
28-
// Initialize a new HttpClient
29-
_client = new HttpClient();
32+
// Initialize a new HttpClient
33+
var client = new HttpClient();
3034

31-
// Add default headers
32-
_client.DefaultRequestHeaders.UserAgent.ParseAdd(Constants.GetHttpUserAgent());
33-
_client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue
34-
{
35-
NoCache = true
36-
};
37-
}
35+
// Add default headers
36+
client.DefaultRequestHeaders.UserAgent.ParseAdd(Constants.GetHttpUserAgent());
37+
client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue
38+
{
39+
NoCache = true
40+
};
3841

39-
return _client;
42+
return client;
4043
}
4144
}
4245
}

0 commit comments

Comments
 (0)