Refresh Token using Polly with Named Client

TL;DR: You need to define a communication protocol between a RetryPolicy, a DelegatingHandler and a TokenService.


In case of Typed Clients you can explicitly call the ExecuteAsync and use the Context to exchange data between the to-be-decorated method and the onRetry(Async) delegate.

This trick can’t be used in a named client situation. What you need to do instead:

  • Separate out the Token management into a dedicated service
  • Use a DelegatingHandler to intercept the HttpClient‘s communication

This sequence diagram depicts the communication between the different components

refreshing token in case of 401

Token Service

The DTO

public class Token
{
    public string Scheme { get; set; }
    public string AccessToken { get; set; }
}

The interface

public interface ITokenService
{
    Token GetToken();
    Task RefreshToken();
}

The dummy implementation

public class TokenService : ITokenService
{
    private DateTime lastRefreshed = DateTime.UtcNow;
    public Token GetToken()
        => new Token { Scheme = "Bearer", AccessToken = lastRefreshed.ToString("HH:mm:ss")}; 

    public Task RefreshToken()
    {
        lastRefreshed = DateTime.UtcNow;
        return Task.CompletedTask;
    }
}

The registration into the DI as Singleton

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<ITokenService, TokenService>();
    ...
}

Delegating Handler

The custom exception

public class OutdatedTokenException : Exception
{

}

The handler (interceptor)

public class TokenFreshnessHandler : DelegatingHandler
{
    private readonly ITokenService tokenService;
    public TokenFreshnessHandler(ITokenService service)
    {
        tokenService = service;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = tokenService.GetToken();
        request.Headers.Authorization = new AuthenticationHeaderValue(token.Scheme, token.AccessToken);

        var response = await base.SendAsync(request, cancellationToken);
        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            throw new OutdatedTokenException();
        }
        return response;
    }
}
  • It retrieves the current token from the TokenService
  • It sets the authorization header
  • It executes the base method
  • It checks the response’s status
    • If 401 then it throws the custom exception
    • If other than 401 then it returns with the response

The registration into the DI as Transient

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<ITokenService, TokenService>();
    services.AddTransient<TokenFreshnessHandler>();
    ...
}

Retry Policy

The policy definition

public IAsyncPolicy<HttpResponseMessage> GetTokenRefresher(IServiceProvider provider)
{
    return Policy<HttpResponseMessage>
        .Handle<OutdatedTokenException>()
        .RetryAsync(async (_, __) => await provider.GetRequiredService<ITokenService>().RefreshToken());
}
  • It receives an IServiceProvider to be able to access the TokenService
  • It performs a single retry if an OutdatedTokenException was thrown
  • Inside the onRetryAsync delegate it calls the TokenService‘s RefreshToken method

Putting all things together

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<ITokenService, TokenService>();
    services.AddTransient<TokenFreshnessHandler>();
    services.AddHttpClient("TestClient")
        .AddPolicyHandler((provider, _) => GetTokenRefresher(provider))
        .AddHttpMessageHandler<TokenFreshnessHandler>();
    ...
}
  • Please bear in mind that the ordering of AddPolicyHandler and AddHttpMessageHandler matters
  • If you would call the AddHttpMessageHandler first and then the AddPolicyHandler in that case your retry would not be triggered

Leave a Comment