Skip to content

Commit c016a78

Browse files
committed
Update Azure Repos REST API class to reuse it's HttpClients
Update the Azure Repos REST API class to only create one `HttpClient` instance, configure common headers, and reuse the same instance for all requests.
1 parent 613ed41 commit c016a78

File tree

5 files changed

+69
-29
lines changed

5 files changed

+69
-29
lines changed

src/Microsoft.AzureRepos/AzureDevOpsRestApi.cs

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ public interface IAzureDevOpsRestApi
2020

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

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

47-
HttpClient client = _httpFactory.GetClient();
48-
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(Constants.Http.MimeTypeJson));
49-
5046
_context.Trace.WriteLine($"HTTP: HEAD {organizationUri}");
51-
using (client)
52-
using (var response = await client.HeadAsync(organizationUri))
47+
using (HttpResponseMessage response = await HttpClient.HeadAsync(organizationUri))
5348
{
5449
_context.Trace.WriteLine("HTTP: Response code ignored.");
5550
_context.Trace.WriteLine("Inspecting headers...");
@@ -113,14 +108,10 @@ public async Task<string> CreatePersonalAccessTokenAsync(Uri organizationUri, st
113108

114109
Uri requestUri = new Uri(identityServiceUri, sessionTokenUrl);
115110

116-
HttpClient client = _httpFactory.GetClient();
117-
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(Constants.Http.MimeTypeJson));
118-
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(Constants.Http.WwwAuthenticateBearerScheme, accessToken);
119-
120111
_context.Trace.WriteLine($"HTTP: POST {requestUri}");
121-
using (client)
122112
using (StringContent content = CreateAccessTokenRequestJson(organizationUri, scopes))
123-
using (var response = await client.PostAsync(requestUri, content))
113+
using (HttpRequestMessage request = CreateRequestMessage(HttpMethod.Post, requestUri, content, accessToken))
114+
using (HttpResponseMessage response = await HttpClient.SendAsync(request))
124115
{
125116
_context.Trace.WriteLine($"HTTP: Response {(int)response.StatusCode} [{response.StatusCode}]");
126117

@@ -161,13 +152,9 @@ private async Task<Uri> GetIdentityServiceUriAsync(Uri organizationUri, string a
161152
Query = locationServiceQuery,
162153
}.Uri;
163154

164-
HttpClient client = _httpFactory.GetClient();
165-
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(Constants.Http.MimeTypeJson));
166-
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(Constants.Http.WwwAuthenticateBearerScheme, accessToken);
167-
168155
_context.Trace.WriteLine($"HTTP: GET {requestUri}");
169-
using (client)
170-
using (var response = await client.GetAsync(requestUri))
156+
using (HttpRequestMessage request = CreateRequestMessage(HttpMethod.Get, requestUri, bearerToken: accessToken))
157+
using (HttpResponseMessage response = await HttpClient.SendAsync(request))
171158
{
172159
_context.Trace.WriteLine($"HTTP: Response {(int)response.StatusCode} [{response.StatusCode}]");
173160
if (response.IsSuccessStatusCode)
@@ -191,6 +178,24 @@ private async Task<Uri> GetIdentityServiceUriAsync(Uri organizationUri, string a
191178

192179
private const RegexOptions CommonRegexOptions = RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase;
193180

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+
194199
/// <summary>
195200
/// Attempt to extract the authority from a Authorization Bearer header.
196201
/// </summary>
@@ -267,6 +272,31 @@ private static StringContent CreateAccessTokenRequestJson(Uri organizationUri, I
267272
return content;
268273
}
269274

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 (!(bearerToken is null))
293+
{
294+
request.Headers.Authorization = new AuthenticationHeaderValue(Constants.Http.WwwAuthenticateBearerScheme, bearerToken);
295+
}
296+
297+
return request;
298+
}
299+
270300
#endregion
271301
}
272302
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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.HeadAsync(uri))
36+
using (HttpResponseMessage response = await HttpClient.HeadAsync(uri))
3837
{
3938
_context.Trace.WriteLine("HTTP: Response code ignored.");
4039

@@ -56,5 +55,8 @@ 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());
5961
}
6062
}

src/Microsoft.Git.CredentialManager/HttpClientFactory.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,28 @@
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-
public HttpClient GetClient()
30+
public HttpClient CreateClient()
2331
{
2432
// Initialize a new HttpClient
2533
var client = new HttpClient();

tests/Microsoft.Git.CredentialManager.Tests/HttpClientFactoryTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public void HttpClientFactory_GetClient_SetsDefaultHeaders()
1212
{
1313
var factory = new HttpClientFactory();
1414

15-
HttpClient client = factory.GetClient();
15+
HttpClient client = factory.CreateClient();
1616

1717
Assert.NotNull(client);
1818
Assert.Equal(Constants.GetHttpUserAgent(), client.DefaultRequestHeaders.UserAgent.ToString());
@@ -24,8 +24,8 @@ public void HttpClientFactory_GetClient_MultipleCalls_ReturnsNewInstance()
2424
{
2525
var factory = new HttpClientFactory();
2626

27-
HttpClient client1 = factory.GetClient();
28-
HttpClient client2 = factory.GetClient();
27+
HttpClient client1 = factory.CreateClient();
28+
HttpClient client2 = factory.CreateClient();
2929

3030
Assert.NotSame(client1, client2);
3131
}

tests/Microsoft.Git.CredentialManager.Tests/Objects/TestHttpClientFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class TestHttpClientFactory : IHttpClientFactory
1010

1111
#region IHttpClientFactory
1212

13-
HttpClient IHttpClientFactory.GetClient()
13+
HttpClient IHttpClientFactory.CreateClient()
1414
{
1515
return new HttpClient(MessageHandler);
1616
}

0 commit comments

Comments
 (0)