Skip to content

Commit db604ef

Browse files
committed
Update HttpClientFactory to use singleton client
Don't create a new `HttpClient` on each `GetClient` call; we run the risk of exhausting all available sockets because a socket is assigned per client instance, and the socket does not become free immediately after disposal of the client. In light of this change to use a singleton instance, don't set the default headers on that singleton client otherwise we will pollute the client setup for other callers. Instead we set the headers per request message that is sent. Tests have been updated to cover the above changes. Additional minor change: move the HTTP-y constants to a nested static class `Constants.Http.*` for better grouping.
1 parent 72a1c6d commit db604ef

File tree

7 files changed

+166
-97
lines changed

7 files changed

+166
-97
lines changed

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,21 @@ public async Task<bool> GetIsSupportedAsync(Uri uri)
3232

3333
bool supported = false;
3434

35-
_context.Trace.WriteLine($"HTTP HEAD {uri}");
35+
_context.Trace.WriteLine($"HTTP: HEAD {uri}");
3636
using (HttpClient client = _httpFactory.GetClient())
37-
using (HttpResponseMessage response = await client.HeadAsync(uri))
37+
using (HttpResponseMessage response = await client.SendAsync(HttpMethod.Head, uri))
3838
{
39-
_context.Trace.WriteLine("HTTP Response - Ignoring response code");
39+
_context.Trace.WriteLine("HTTP: Response code ignored.");
4040

4141
_context.Trace.WriteLine("Inspecting WWW-Authenticate headers...");
4242
foreach (AuthenticationHeaderValue wwwHeader in response.Headers.WwwAuthenticate)
4343
{
44-
if (StringComparer.OrdinalIgnoreCase.Equals(wwwHeader.Scheme, Constants.WwwAuthenticateNegotiateScheme))
44+
if (StringComparer.OrdinalIgnoreCase.Equals(wwwHeader.Scheme, Constants.Http.WwwAuthenticateNegotiateScheme))
4545
{
4646
_context.Trace.WriteLine("Found WWW-Authenticate header for Negotiate");
4747
supported = true;
4848
}
49-
else if (StringComparer.OrdinalIgnoreCase.Equals(wwwHeader.Scheme, Constants.WwwAuthenticateNtlmScheme))
49+
else if (StringComparer.OrdinalIgnoreCase.Equals(wwwHeader.Scheme, Constants.Http.WwwAuthenticateNtlmScheme))
5050
{
5151
_context.Trace.WriteLine("Found WWW-Authenticate header for NTLM");
5252
supported = true;

src/Microsoft.Git.CredentialManager/Constants.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
3-
using System.Text;
43

54
namespace Microsoft.Git.CredentialManager
65
{
@@ -9,9 +8,6 @@ public static class Constants
98
public const string GcmVersion = "1.0";
109
public const string PersonalAccessTokenUserName = "PersonalAccessToken";
1110

12-
public const string WwwAuthenticateNegotiateScheme = "Negotiate";
13-
public const string WwwAuthenticateNtlmScheme = "NTLM";
14-
1511
public static class EnvironmentVariables
1612
{
1713
public const string GcmTrace = "GCM_TRACE";
@@ -20,6 +16,12 @@ public static class EnvironmentVariables
2016
public const string GitTerminalPrompts = "GIT_TERMINAL_PROMPT";
2117
}
2218

19+
public static class Http
20+
{
21+
public const string WwwAuthenticateNegotiateScheme = "Negotiate";
22+
public const string WwwAuthenticateNtlmScheme = "NTLM";
23+
}
24+
2325
/// <summary>
2426
/// Get standard program header title for Git Credential Manager, including the current version and OS information.
2527
/// </summary>

src/Microsoft.Git.CredentialManager/HttpClientExtensions.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33
using System;
4+
using System.Collections.Generic;
45
using System.Net.Http;
56
using System.Threading;
67
using System.Threading.Tasks;
@@ -52,5 +53,31 @@ public static Task<HttpResponseMessage> HeadAsync(this HttpClient client, Uri re
5253
}
5354

5455
#endregion
56+
57+
#region SendAsync with content and headers
58+
59+
public static Task<HttpResponseMessage> SendAsync(this HttpClient client,
60+
HttpMethod method,
61+
Uri requestUri,
62+
IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers = null,
63+
HttpContent content = null)
64+
{
65+
var request = new HttpRequestMessage(method, requestUri)
66+
{
67+
Content = content
68+
};
69+
70+
if (!(headers is null))
71+
{
72+
foreach (var kvp in headers)
73+
{
74+
request.Headers.Add(kvp.Key, kvp.Value);
75+
}
76+
}
77+
78+
return client.SendAsync(request);
79+
}
80+
81+
#endregion
5582
}
5683
}
Lines changed: 13 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
3-
using System.Collections.Generic;
43
using System.Net.Http;
54
using System.Net.Http.Headers;
65

@@ -20,57 +19,24 @@ public interface IHttpClientFactory
2019

2120
public class HttpClientFactory : IHttpClientFactory
2221
{
23-
public HttpClient GetClient()
24-
{
25-
var client = new HttpClient();
26-
27-
// Add default headers
28-
client.DefaultRequestHeaders.UserAgent.ParseAdd(Constants.GetHttpUserAgent());
29-
client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue
30-
{
31-
NoCache = true
32-
};
22+
private HttpClient _client;
3323

34-
return client;
35-
}
36-
}
37-
38-
public static class HttpClientFactoryExtensions
39-
{
40-
/// <summary>
41-
/// Get an instance of <see cref="HttpClient"/> with default headers and a bearer token based authorization header.
42-
/// </summary>
43-
/// <param name="factory"><see cref="IHttpClientFactory"/></param>
44-
/// <param name="bearerToken">Bearer token value</param>
45-
/// <returns>Client with default and bearer token headers.</returns>
46-
public static HttpClient GetClient(this IHttpClientFactory factory, string bearerToken)
47-
{
48-
var client = factory.GetClient();
49-
50-
if (bearerToken != null)
51-
{
52-
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
53-
}
54-
55-
return client;
56-
}
57-
58-
/// <summary>
59-
/// Get an instance of <see cref="HttpClient"/> with default headers and additional custom headers.
60-
/// </summary>
61-
/// <param name="factory"><see cref="IHttpClientFactory"/></param>
62-
/// <param name="headers">Custom HTTP headers to set on the client.</param>
63-
/// <returns>Client with default and custom headers.</returns>
64-
public static HttpClient GetClient(this IHttpClientFactory factory, IDictionary<string, IEnumerable<string>> headers)
24+
public HttpClient GetClient()
6525
{
66-
var client = factory.GetClient();
67-
68-
foreach (var header in headers)
26+
if (_client is null)
6927
{
70-
client.DefaultRequestHeaders.Add(header.Key, header.Value);
28+
// Initialize a new HttpClient
29+
_client = new HttpClient();
30+
31+
// Add default headers
32+
_client.DefaultRequestHeaders.UserAgent.ParseAdd(Constants.GetHttpUserAgent());
33+
_client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue
34+
{
35+
NoCache = true
36+
};
7137
}
7238

73-
return client;
39+
return _client;
7440
}
7541
}
7642
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Net.Http;
4+
using System.Threading.Tasks;
5+
using Microsoft.Git.CredentialManager.Tests.Objects;
6+
using Xunit;
7+
8+
namespace Microsoft.Git.CredentialManager.Tests
9+
{
10+
public class HttpClientExtensionsTests
11+
{
12+
[Fact]
13+
public async Task HttpClientExtensions_SendAsync_SendsRequestMessage()
14+
{
15+
var method = HttpMethod.Get;
16+
var uri = new Uri("http://example.com");
17+
18+
var httpHandler = new TestHttpMessageHandler{ThrowOnUnexpectedRequest = true};
19+
httpHandler.Setup(method, uri, request =>
20+
{
21+
Assert.Equal(method, request.Method);
22+
Assert.Equal(uri, request.RequestUri);
23+
24+
return new HttpResponseMessage();
25+
});
26+
27+
var httpClient = new HttpClient(httpHandler);
28+
29+
await HttpClientExtensions.SendAsync(httpClient, method, uri);
30+
}
31+
32+
[Fact]
33+
public async Task HttpClientExtensions_SendAsync_Content_SetsContent()
34+
{
35+
var method = HttpMethod.Get;
36+
var uri = new Uri("http://example.com");
37+
38+
var expectedContent = new StringContent("foobar");
39+
40+
var httpHandler = new TestHttpMessageHandler{ThrowOnUnexpectedRequest = true};
41+
httpHandler.Setup(method, uri, request =>
42+
{
43+
Assert.Same(expectedContent,request.Content);
44+
45+
return new HttpResponseMessage();
46+
});
47+
48+
var httpClient = new HttpClient(httpHandler);
49+
50+
await HttpClientExtensions.SendAsync(httpClient, method, uri, null, expectedContent);
51+
}
52+
53+
[Fact]
54+
public async Task HttpClientExtensions_SendAsync_Headers_SetsHeaders()
55+
{
56+
var method = HttpMethod.Get;
57+
var uri = new Uri("http://example.com");
58+
59+
var customHeaders = new Dictionary<string, IEnumerable<string>>
60+
{
61+
["header0"] = new string[0],
62+
["header1"] = new []{ "first-value" },
63+
["header2"] = new []{ "first-value", "second-value"},
64+
["header3"] = new []{ "first-value", "second-value", "third-value"},
65+
};
66+
67+
var httpHandler = new TestHttpMessageHandler{ThrowOnUnexpectedRequest = true};
68+
httpHandler.Setup(method, uri, request =>
69+
{
70+
Assert.False(request.Headers.Contains("header0"));
71+
Assert.True(request.Headers.Contains("header1"));
72+
Assert.True(request.Headers.Contains("header2"));
73+
Assert.True(request.Headers.Contains("header3"));
74+
Assert.Equal(customHeaders["header1"], request.Headers.GetValues("header1"));
75+
Assert.Equal(customHeaders["header2"], request.Headers.GetValues("header2"));
76+
Assert.Equal(customHeaders["header3"], request.Headers.GetValues("header3"));
77+
78+
return new HttpResponseMessage();
79+
});
80+
81+
var httpClient = new HttpClient(httpHandler);
82+
83+
await HttpClientExtensions.SendAsync(httpClient, method, uri, customHeaders);
84+
}
85+
}
86+
}
Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
3-
using System.Collections.Generic;
43
using System.Net.Http;
54
using Xunit;
65

@@ -21,43 +20,14 @@ public void HttpClientFactory_GetClient_SetsDefaultHeaders()
2120
}
2221

2322
[Fact]
24-
public void HttpClientFactory_GetClient_BearerToken_SetsAuthorizationBearerTokenHeader()
23+
public void HttpClientFactory_GetClient_MultipleCalls_ReturnsSameInstance()
2524
{
26-
const string bearerToken = "letmein123";
27-
2825
var factory = new HttpClientFactory();
2926

30-
HttpClient client = factory.GetClient(bearerToken);
31-
32-
Assert.NotNull(client);
33-
Assert.NotNull(client.DefaultRequestHeaders.Authorization);
34-
Assert.Equal("Bearer", client.DefaultRequestHeaders.Authorization.Scheme);
35-
Assert.Equal(bearerToken, client.DefaultRequestHeaders.Authorization.Parameter);
36-
}
27+
HttpClient client1 = factory.GetClient();
28+
HttpClient client2 = factory.GetClient();
3729

38-
[Fact]
39-
public void HttpClientFactory_GetClient_CustomHeaders_SetsHeaders()
40-
{
41-
var customHeaders = new Dictionary<string, IEnumerable<string>>
42-
{
43-
["header0"] = new string[0],
44-
["header1"] = new []{ "first-value" },
45-
["header2"] = new []{ "first-value", "second-value"},
46-
["header3"] = new []{ "first-value", "second-value", "third-value"},
47-
};
48-
49-
var factory = new HttpClientFactory();
50-
51-
HttpClient client = factory.GetClient(customHeaders);
52-
53-
Assert.NotNull(client);
54-
Assert.False(client.DefaultRequestHeaders.Contains("header0"));
55-
Assert.True(client.DefaultRequestHeaders.Contains("header1"));
56-
Assert.True(client.DefaultRequestHeaders.Contains("header2"));
57-
Assert.True(client.DefaultRequestHeaders.Contains("header3"));
58-
Assert.Equal(customHeaders["header1"], client.DefaultRequestHeaders.GetValues("header1"));
59-
Assert.Equal(customHeaders["header2"], client.DefaultRequestHeaders.GetValues("header2"));
60-
Assert.Equal(customHeaders["header3"], client.DefaultRequestHeaders.GetValues("header3"));
30+
Assert.Same(client1, client2);
6131
}
6232
}
6333
}

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

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,42 @@
66
using System.Net.Http;
77
using System.Threading;
88
using System.Threading.Tasks;
9+
using Microsoft.Git.CredentialManager.Authentication;
910
using Xunit;
1011

1112
namespace Microsoft.Git.CredentialManager.Tests.Objects
1213
{
1314
public class TestHttpMessageHandler : HttpMessageHandler
1415
{
15-
private readonly IDictionary<(HttpMethod method, Uri uri), HttpResponseMessage> _handlers =
16-
new Dictionary<(HttpMethod, Uri), HttpResponseMessage>();
16+
public delegate HttpResponseMessage RequestHandler(HttpRequestMessage request);
17+
18+
private readonly IDictionary<(HttpMethod method, Uri uri), RequestHandler> _handlers =
19+
new Dictionary<(HttpMethod, Uri), RequestHandler>();
1720

1821
private readonly IDictionary<(HttpMethod method, Uri uri), int> _requestCounts =
19-
new Dictionary<(HttpMethod method, Uri uri), int>();
22+
new Dictionary<(HttpMethod, Uri), int>();
2023

2124
public bool ThrowOnUnexpectedRequest { get; set; }
25+
public bool SimulateNoNetwork { get; set; }
26+
27+
public void Setup(HttpMethod method, Uri uri, RequestHandler handler)
28+
{
29+
_handlers[(method, uri)] = handler;
30+
}
2231

2332
public void Setup(HttpMethod method, Uri uri, HttpResponseMessage responseMessage)
2433
{
25-
_handlers[(method, uri)] = responseMessage;
34+
_handlers[(method, uri)] = _ => responseMessage;
2635
}
2736

2837
public void Setup(HttpMethod method, Uri uri, HttpStatusCode responseCode)
2938
{
30-
Setup(method, uri, new HttpResponseMessage(responseCode));
39+
Setup(method, uri, responseCode, string.Empty);
40+
}
41+
42+
public void Setup(HttpMethod method, Uri uri, HttpStatusCode responseCode, string content)
43+
{
44+
Setup(method, uri, new HttpResponseMessage(responseCode){Content = new StringContent(content)});
3145
}
3246

3347
public void AssertRequest(HttpMethod method, Uri uri, int expectedNumberOfCalls)
@@ -49,12 +63,16 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
4963

5064
IncrementRequestCount(requestKey);
5165

66+
if (SimulateNoNetwork)
67+
{
68+
throw new HttpRequestException("Simulated no network");
69+
}
70+
5271
foreach (var kvp in _handlers)
5372
{
5473
if (kvp.Key == requestKey)
5574
{
56-
HttpResponseMessage response = kvp.Value;
57-
return Task.FromResult(response);
75+
return Task.FromResult(kvp.Value(request));
5876
}
5977
}
6078

0 commit comments

Comments
 (0)