Search code examples
c#dotnet-httpclientpollyihttpclientfactorydelegatinghandler

Add dynamic parameter to DelegatingHandler used within the HttpClientFactory Service


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?


Solution

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