Search code examples
c#azure-functionspollyretry-logicmemorycache

Should I use IMemoryCache to store the bearer token or implement a Polly retry policy for 401 errors in Azure Function?


I am working with Service Bus Topic Trigger Azure Function, and I need to handle 401 (Unauthorized) errors when making HTTP requests to external API, such as refreshing a bearer token and retrying the failed request.

I am considering two approaches and I am unsure which one is more suitable. I would appreciate some guidance on which approach is best.

This is my retry policy class containing unauthorized retry policy for 401 error.

public class PollyRetryPolicy : IPollyRetryPolicy
{
    private readonly ILogger<PollyRetryPolicy> _logger;
    private readonly IOptions<RetryPolicyConfigOptions> _options;
   

    public PollyRetryPolicy(ILogger<PollyRetryPolicy> logger,
        IOptions<RetryPolicyConfigOptions> options)
    {
        _logger = logger;
        _options = options;
        _jitterer = new Random();
    }

    public IAsyncPolicy GetUnauthorizedRetryPolicy()
    {
        var policy = Policy
            .Handle<HttpRequestException>(ex => ex.StatusCode == HttpStatusCode.Unauthorized)
            .WaitAndRetryAsync(
                _options.Value.MaxRetryAttempts,
                retryAttempt => TimeSpan.Zero); // Set the delay to zero milliseconds for immediate retry
        return policy;
    }
}

This is my api client where I am trying to call first bearer token endpoint and using IMemoryCache to store bearer token and then making the actual call with this bearer token.

public class NotificationApiClient : INotificationApiClient
{
    
    private readonly ILogger<NotificationApiClient> _logger;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IMemoryCache _memoryCache;
    private readonly NotificationApiConfigOption _notificationApiConfigOption;

    
    public NotificationApiClient(ILogger<NotificationApiClient> logger,
        IHttpClientFactory httpClientFactory,
        IMemoryCache memoryCache,
        IOptions<NotificationApiConfigOption> notificationApiConfigOption)
    {
        _logger = logger;
        _httpClientFactory = httpClientFactory;
        _memoryCache = memoryCache;
        _notificationApiConfigOption = notificationApiConfigOption.Value;
    }

    
    public async Task<bool> CheckDeliveryAccessAsync(int code)
    {
        try
        {
            
            var httpClient = _httpClientFactory.CreateClient();
            httpClient.BaseAddress = new Uri(_notificationApiConfigOption.BaseUri);
            
            // Get the bearer token
            string bearerToken = await GetNotificationAccessTokenAsync();
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
            var apiResponse = await httpClient.GetAsync($"{_notificationApiConfigOption.CheckDeliveryAccessApiUri}?code={code}");
            if (apiResponse.StatusCode == HttpStatusCode.OK)
            {
                string resultContent = await apiResponse.Content.ReadAsStringAsync();
                var result = resultContent.AsPoco<CheckInvoiceDeliveryAccessResponse>();
                return result.Data.IsActive;
            }
            else
            {
                return false;
            }
        }
        catch (Exception ex)
        {
            
            throw;
        }
    }

   
    public async Task<string> GetNotificationAccessTokenAsync()
    {
        try
        {
            
            if (_memoryCache.TryGetValue("BearerToken", out string cachedBearerToken))
            {
                return cachedBearerToken;
            }
            var httpClient = _httpClientFactory.CreateClient();
            httpClient.BaseAddress = new Uri(_notificationApiConfigOption.BaseUri);
            
            var formData = new Dictionary<string, string>
            {
                { "clientId", _notificationApiConfigOption.ClientId },
                { "clientSecret", _notificationApiConfigOption.ClientSecret }
            };
            
            var content = new FormUrlEncodedContent(formData);
            var apiResponse = await httpClient.PostAsync($"/{_notificationApiConfigOption.AccessTokenUri}", content);
            
            if (apiResponse.StatusCode == HttpStatusCode.OK)
            {
                string resultContent = await apiResponse.Content.ReadAsStringAsync();
                
                var result = resultContent.AsPoco<NotificationAccessTokenResponse>();
                
                _memoryCache.Set("BearerToken", result.AccessToken, new MemoryCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(result.ExpiresIn)
                });
                
                return result.AccessToken;
            }
            else
            {
               
                return string.Empty;
            }
        }
        catch (Exception ex)
        {
            
            throw;
        }
    }
}

Solution

  • As @codebrane noted in the comments section you can also proactively retrieve the new token before it expires.

    Both proactive and reactive approaches has its own pros and cons. I would suggest to use the proactive approach if your services are customer-facing. With this approach the user requests should not be retried due to expired service tokens.

    The reactive approach could be useful if the communication between services are not continuous rather happens on-demand. And the retry imposed latency is acceptable.

    Here I've detailed 3 different ways how to achieve it: Refresh Token using Polly with Named Client


    One note regarding to this piece:

    var policy = Policy
        .Handle<HttpRequestException>(ex => ex.StatusCode == HttpStatusCode.Unauthorized)
        .WaitAndRetryAsync(
            _options.Value.MaxRetryAttempts,
            retryAttempt => TimeSpan.Zero); // Set the delay to zero milliseconds for immediate retry
    

    you could use RetryAsync if you don't want to wait between the retry attempts

    var policy = Policy
        .Handle<HttpRequestException>(ex => ex.StatusCode == HttpStatusCode.Unauthorized)
        .RetryAsync(_options.Value.MaxRetryAttempts);
    

    UPDATE #1

    My application is not customer facing, It is kind of background job to trigger notifications based on the events. So in this case should I use proactive or Should I go with reactive?

    As always it depends. :) Let me try to help you with a trade-off analysis.

    Reactive approach

    Pros

    • It is triggered on demand. If there is no traffic then no token-refreshment is performed
    • If your system has a reactive nature then it is easier to reason about this approach

    Cons

    • If the token retrieval/refreshment usually takes longer than the original request then the added delay (due to token service call) becomes tangible
    • If the token expires during a burst then many requests are halted (queued) until the new token becomes available

    Proactive approach

    Pros

    • From the incoming request processing perspective your system behaves like you have a long-living token
    • Depending on the token service implementation it might allow multiple tokens (one for each downstream service) retrieval at once

    Cons

    • If the token service is rate limited / imposes quota then this approach is more likely to exceed the limit than the reactive one
    • You might also need to decorate your token service call with a retry to overcome transient failures

    Side-note: In both cases you have to make sure that there are no concurrent retrieval calls. In other words, you refresh the token only by a single thread. It is easier to guarantee this with the proactive approach.