HttpClient in .NetCore —— HttpClient thread safe & HttpRequestHeaders thread not safe

Concurrency with HttpClient | Think Programming

 

1. 如果我们以无状态的方式使用它,HttpClient可以被视为线程安全

2. HttpClient 中的属性不是线程安全的,特别是对于 DefaultRequestHeaders 属性

大多数情况下,我们以状态方式使用HttpClient,例如,在HttpRequestHeaders  中添加一个token

 

 

HttpClient is well known for being thread safe. Infact, it is very much encouraged to instantiate a object and keep it in memory for as long as you need, this avoids the extra cost needed for reconstructing the object.HttpClient

 

But, is it really that thread safe? Well, after some experiments with high volume of requests, it turns out – not completely / it depends.

 

Stateless

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 is only used to send the request and discarded immediately after.stringContent

 

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

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);
    }
}

  

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 at no point does not guarantee thread safety of shared state – particularly Http Headers.HttpClient

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 ‘s internal shared state, notably Default Header.HttpClient

 

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.

 

如何使用 HttpClient 作为单例,但仍然处于线程安全状态

关键点是利用类 HttpRequestMessage 来保存 request Headers.

若要为所有请求添加不变的标头,请使用 DefaultRequestHeaders
image.png

要为特定请求添加动态标头,请使用 HttpRequestMessage
image.png

 

posted @ 2022-07-11 15:48  PanPan003  阅读(161)  评论(0编辑  收藏  举报