1

I'm trying to use a IHttpClientFactory in my application in order to efficiently manage the HTTP Clients used throughout its lifecycle.

Each of these clients need to have their bearer token set up depending on a specific Client ID which is only known at runtime (at any point we can have several Client IDs in use), so essentially this is a parameter I have to use to get the Authorization header. Because these headers expire frequently (something I have no control over when it will happen), I want to reset the Token Header and retrying the call on a 401 status response.

I tried using an HTTPClientService to create them which receives an IHTTPClientFactory through Dependency Injection:


    internal class HttpClientService
    {
        private readonly IHttpClientFactory _httpClientFactory;

        public HttpClientService(IHttpClientFactory httpClientFactory)
        {
            _httpClientFactory = httpClientFactory;
        }

        //string should be the Namespace????
        public async Task<HttpClient> GetHttpClientAsync(string clientId)
        {

            var client = _httpClientFactory.CreateClient("BasicClient");
            //get token
            client.DefaultRequestHeaders.Authorization = await GetBearerTokenHeader(clientId);

            return client;
        }
    }

Because tokens expire frequently (something I have no control over when it will happen), I want to reset the Token Header and retrying the call on a 401 status response, so I'm using a custom HttpMessageHandler to do just this:

services.AddHttpClient("BasicClient").AddPolicyHandler(GetRetryPolicy()).AddHttpMessageHandler<TokenFreshnessHandler>();

TokenFreshnessHandler class:

public class TokenFreshnessHandler : DelegatingHandler
    {
        public TokenFreshnessHandler()
        {
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var response = await base.SendAsync(request, cancellationToken);

            if (response.StatusCode != HttpStatusCode.Unauthorized)
            {
                //REFRESH TOKEN AND SEND AGAIN!!!
                request.Headers.Authorization = await GetBearerTokenHeader(/** CLIENT ID  CANNOT BE RETRIEVED**/);
                response = await base.SendAsync(request, cancellationToken);
            }
            return response;
        }
    }

But unfortunately, by the time I get a chance to reset the token header, I have no way of knowing the original ClientID parameter

Is there an effective way of making my ClientID accessible to the HTTPRequestMessage?

Can I somehow use my HTTPClient to pass some values into the Options property of an HTTPRequest message?

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • I'd like to suggest to add the clientId as a http-header and then get this header in the handler. Inside the handler, check for this clientId header, add the authorization and than remove the header. – Bruno Warmling Apr 28 '23 at 17:54
  • Hey Bruno, thanks for the idea, I thought of that, too, but it's a bit "hacky", I was hoping there's a better way to do it, or even another approach entirely – user14101378 Apr 28 '23 at 18:08

2 Answers2

0

Well, there are some ways to do that, of course, without changing the request.

But it all depends on how you are going to use that HttpClientServiceand how it is registered in the DI.

So for example, you could have a "session" object in the DI which is registered as singleton, but the value is treated as local, so that you could use a factory to get the most updated version of it.

public class ClientSession
{ 
    private AsyncLocal<string> clientId { get; set; }
    public string? ClientId 
    { 
        get => this.clientId?.Value; 
        set => this.clientId?.Value = value; 
    }
}

public class ClientSessionFactory : IClientSessionFactory
{
    private readonly IServiceProvider serviceProvider;

    public ClientSessionFactory(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }
    
    public ClientSession Create()
    {
        return serviceProvider.GetRequiredService<ClientSession>();
    }
}

So, this factory can be injected inside the HttpHandler:

private readonly IClientSessionFactory ClientSessionFactory;

public TokenFreshnessHandler(IClientSessionFactory ClientSessionFactory)
{
    this.ClientSessionFactory = ClientSessionFactory;
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
    var clientSession = ClientSessionFactory.Create();
}

And you can set the client id before create your client:

var clientSession = ClientSessionFactory.Create();
clientSession.ClientId = "xpto";
var httpClient = httpClientService.GetHttpClientAsync();

That's the idea.

Also, you could adapt it to use like this:

using (var clientSession = ClientSessionFactory.Create())
{
    clientSession.ClientId = "xpto";
    var httpClient = httpClientService.GetHttpClientAsync();
}

But then you have to implement some disposing mechanism.

Bruno Warmling
  • 362
  • 4
  • 14
  • Hey Bruno, interesting thought, but would this be a problem if I try to use 2 HTTPClients in the same await context? – user14101378 Apr 29 '23 at 11:00
0

When passing data from one DelegatingHandler to another in regards to a HttpRequestMessage, use the Options property.

But when passing data from one Polly policy to another (wrapped by use of PolicyWrap or chained policies on the HttpClient), then you should use Polly's Context feature. This can be done through a convenient extension method.

In your host setup code:

services.AddHttpClient<IHttpService, HttpService>()
        .AddPolicyHandler(_ => Policy<HttpResponseMessage>.HandleResult(r => r.StatusCode == HttpStatusCode.Unauthorized)
                                                          .RetryAsync(1))
        .AddHttpMessageHandler<AuthorizationMessageHandler>();

Your HTTP service that wraps an HTTP client:

public class HttpService : IHttpService
{
    private readonly HttpClient _httpClient;

    public HttpService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<ApiResponse?> GetResource()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, "https://myurl.com");

        return await Get<ApiResponse>(request);
    }

    private async Task<TResponse?> Get<TResponse>(HttpRequestMessage request)
        where TResponse : class
    {
        request.SetPolicyExecutionContext(new Context { ["clientId"] = "myClientId" });

        var responseMessage = await _httpClient.SendAsync(request);
        var response = await JsonSerializer.DeserializeAsync<TResponse>(await responseMessage.Content.ReadAsStreamAsync());

        return response;
    }
}

public interface IHttpService
{
    Task<ApiResponse?> GetResource();
}

public class ApiResponse { }

Your AuthorizationMessageHandler that ensures that the token is always set on the request message:

public class AuthorizationMessageHandler : DelegatingHandler
{
    private readonly ITokenAcquisitionService _tokenAcquisitionService;

    public AuthorizationMessageHandler(ITokenAcquisitionService tokenAcquisitionService)
    {
        _tokenAcquisitionService = tokenAcquisitionService;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var context = request.GetPolicyExecutionContext();
        var clientId = context?["clientId"] as string ?? throw new InvalidOperationException("No clientId found in execution context");

        var token = await _tokenAcquisitionService.GetToken(clientId);
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);

        return await base.SendAsync(request, cancellationToken);
    }
}

The interface for a service that acquires an authorization token:

public interface ITokenAcquisitionService
{
    Task<string> GetToken(string clientId);
}
silkfire
  • 24,585
  • 15
  • 82
  • 105
  • Thanks for the reply, this is indeed something that would work in a lot of cases, but in my situation, I also need to pass my HTTP Client as argument to other libraries... I was thinking I could create a dummy class which inherits HTTPClient and also contains in a property the HTTPClient created by IHTTPClientFactory. This way, I can override SendAsync and store the ClientID in my new class and use it there. But essentially, I'd have to instantiate the base constructor of the new class with a "DummyHandler" and sneakly pass all requests to a new client, which is an unfortunate design. – user14101378 Apr 29 '23 at 10:57
  • I'd really appreciate any feedback on the idea above. From what I read about HTTPClients, I think it would work, but from a best practices perspective, it feels like a hack :) – user14101378 Apr 29 '23 at 11:00
  • I believe that if your approach requires you to pass around the same client to other services then there's a flaw in your design. An `HttpClient` is simply a wrapper around a pipeline of message handlers. What are you trying to achieve? – silkfire Apr 29 '23 at 20:41
  • Unfortunately I'm constrained to use a library which requires me to pass these http clients as parameters (at one time I will have several ClientIDs that might require an HTTPClient). They are used only for a short amount of time and they can be disposed afterwards, thus the IHTTPClientFactory seems to be an ideal choice, but this lack of configuration is a bit blocking – user14101378 Apr 30 '23 at 06:53