I’ve recently been heavily involved in moving towards a Service Oriented Architecture – and the most common form of Transport / Network Protocol used is of course, HTTP
due to tooling and statelessness.
HttpClient
is well known for being thread safe. Infact, it is very much encouraged to instantiate a HttpClient
object and keep it in memory for as long as you need, this avoids the extra cost needed for reconstructing the object.
But, is it really that thread safe? Well, after some experiments with high volume of requests, it turns out – not completely / it depends.
Stateless
Let’s take a look at how we do a POST
with HttpClient
.
var httpClient = new HttpClient();
var stringContent = new StringContent("{yes=true}", Encoding.UTF8);
httpClient.PostAsync("http://mydomain.com/api/search", stringContent);
var result = httpClient.PostAsync("request", stringContent);
DoSomethingToOurResult(result.Result);
In this case, it would be sound to assume that stringContent
is only used to send the request and discarded immediately after.
Shared Headers / State
HttpClient
also has a way of sharing common things for every request, such as a header – which can include Authentication token.
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenService.GetToken());
var stringContent = new StringContent("{yes=true}", Encoding.UTF8);
var result = httpClient.PostAsync("http://mydomain.com/api/search", stringContent);
DoSomethingToOurResult(result.Result);
This is great, now we no longer have to set our token in the header every single time we do a request to our service.
Adding our Thread Safe Service
Now, let’s try something else. Let’s say, we want a service class that gives us an always up-to-date valid authtoken, so this service essentially manages the life-time of the token and renews it for us.
public class TokenService
{
private DateTime _nextRenewal = DateTime.MinValue;
private string _token;
private object _mutex;
public string GetToken()
{
if (_nextRenewal <= DateTime.UtcNow)
{
lock (_mutex)
{
if (_nextRenewal <= DateTime.UtcNow)
{
RenewToken();
}
}
}
return _token;
}
private void RenewToken()
{
_token = Guid.NewGuid().ToString();
_nextRenewal = DateTime.UtcNow.AddHours(1);
}
}
Putting it together
Ok, we have a hypothetical code that guarantee’s to return valid token 100% of the time. Let’s use it.
var httpClient = singletonHttpClient;
var tokenService = singletonTokenService;
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenService.GetToken());
var stringContent = new StringContent("{yes=true}", Encoding.UTF8);
var result = httpClient.PostAsync("http://mydomain.com/api/search", stringContent);
DoSomethingToOurResult(result.Result);
When we run this, most of the time (99.9% of the cases), this will work – up until this code runs at almost exactly the same moment in time.
The Problem
As with most problems with concurrency, the problem is the shared state. As it turns out HttpClient
at no point does not guarantee thread safety of shared state – particularly Http Headers.
Investigating this further, we find that the headers are implemented internally as a List
, not to mention the other parts used which aren’t meant to handle concurrency well i.e. singleton without use of locks etc.
The Fix
In this particular case, the fix is fairly straight forward – don’t use HttpClient
‘s internal shared state, notably Default Header.
Let’s add an extension method so we can do the same thing as we previously have in a stateless manner.
public static class HttpClientExtensions
{
public static async Task<HttpResponseMessage> PostAsync(
this HttpClient @this,
string url,
HttpContent content,
Action<HttpRequestHeaders> beforeRequest)
{
var request = new HttpRequestMessage(HttpMethod.Post, url);
beforeRequest(request.Headers);
request.Content = content;
return await @this.SendAsync(request);
}
}
Now, let’s make use of the stateless way of doing this.
var httpClient = singletonHttpClient;
var tokenService = singletonTokenService;
var stringContent = new StringContent("{yes=true}", Encoding.UTF8);
var result = httpClient.PostAsync("request", stringContent, (h) =>
{
h.Authorization = new AuthenticationHeaderValue("Bearer", tokenService.GetToken());
});
DoSomethingToOurResult(result.Result);
Our solution is now completely thread-safe.
Valuable lesson here is, always check thread-safety.
This is really where functional paradigm really shines, so maybe we should start using that too for these types of problems more frequently.