(My answer is not specific to Infusionsoft).
Here's an approach I use to prevent duplicate concurrent requests to renew/refresh a bearer-token when a web-service client may make concurrent requests that use the same bearer-token (to prevent each request thread or async-context from making its own separate refresh-request).
The trick is to use a cached Task<AuthResponseDto>
(where AuthResponseDto
is the DTO type containing the latest successfully obtained access_token
) that's swapped inside a lock
(you can't await
inside a lock
, but you can copy Task
references inside a lock
and then await
outside of the lock
).
// NOTE: `ConfigureAwait(false)` calls omitted for brevity. You should re-add them back.
class MyHttpClientWrapper
{
private readonly String refreshTokenOrClientCredentialsOrWhatever;
private readonly IHttpClientFactory hcf;
private readonly Object lastAuthTaskLock = new Object();
private Task<AuthResponseDto> lastAuthTask;
private DateTime lastAuthTaskAt;
public MyHttpClientWrapper( IHttpClientFactory hcf )
{
this.hcf = hcf ?? throw new ArgumentNullException( nameof(hcf) );
this.refreshTokenOrClientCredentialsOrWhatever = LoadFromSavedConfig();
}
private async Task<AuthResponseDto> RefreshBearerTokenAsync()
{
using( HttpClient hc = this.hcf.CreateClient() )
using( HttpResponseMessage resp = await hc.PostAsync( this.refreshTokenOrClientCredentialsOrWhatever ) )
{
AuthResponseDto ar = await DeserializeJsonResponseAsync( resp );
this.lastAuthTaskExpiresAt = DateTime.UtcNow.Add( ar.MaxAge );
return ar;
}
}
private async Task<String> RefreshBearerTokenIfNecessaryAsync()
{
Task<AuthResponseDto> task;
lock( this.lastAuthTaskLock )
{
if( this.lastAuthTask is null )
{
// e.g. This is the first ever request.
task = this.lastAuthTask = this.RefreshBearerTokenAsync();
}
else
{
task = this.lastAuthTask;
// Is the task currently active? If it's currently busy then just await it (thus preventing duplicate requests!)
if( task.IsCompleted )
{
// If the current bearer-token is definitely expired, then replace it:
if( this.lastAuthTaskExpiresAt <= DateTime.UtcNow )
{
task = this.lastAuthTask = this.RefreshBearerTokenAsync();
}
}
else
{
// Continue below.
}
}
}
AuthResponseDto ar = await task;
return ar.BearerToken;
}
//
public async Task<CustomerDto> GetCustomerAsync( Int32 customerId )
{
// Always do this in every request to ensure you have the latest bearerToken:
String bearerToken = await this.RefreshBearerTokenIfNecessaryAsync();
using( HttpClient hc = this.hcf.Create() )
using( HttpRequestMessage req = new HttpRequestMessage() )
{
req.Headers.Add( "Authorization", "Bearer " + bearerToken );
using( HttpResponseMessage resp = await hc.SendAsync( req ) )
{
if( resp.StatusCode == 401 )
{
// Authentication error before the token expired - invoke and await `RefreshBearerTokenAsync` (rather than `RefreshBearerTokenIfNecessaryAsync`) and see what happens. If it succeeds then re-run `req`) otherwise throw/fail because that's an unrecoverable error.
}
// etc
}
}
}
}