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;
}
}
}
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:
you could use
RetryAsync
if you don't want to wait between the retry attemptsUPDATE #1
As always it depends. :) Let me try to help you with a trade-off analysis.
Reactive approach
Pros
Cons
Proactive approach
Pros
Cons
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.