I'm using flurl and polly as part of my code to post asynchronously - I'm having an issue with the retry policy and the file content getting disposed, on a retry the FileContent
option has been disposed.
I will get the following error:
System.ObjectDisposedException: Cannot access a disposed object. Object name: 'Flurl.Http.Content.FileContent'.
If someone could advise where best to place the logic to ensure FileContent
is available for the retry
var endpoint = $"api endpoint here";
if (!File.Exists(filePath))
{
return Task.CompletedTask;
}
var fileBytesLength = _fileSystem.FileInfo.FromFileName(filePath).Length;
var httpContent = new Flurl.Http.Content.FileContent(filePath, (int)fileBytesLength);
return _statusRetryPolicy
.StatusAsyncRetryPolicy()
.ExecuteAsync(() => endpoint
.WithOAuthBearerToken(accessToken)
.WithTimeout(TimeSpan.FromMinutes(timeOutMinutes))
.WithHeader("Content-Type", "application/zip")
.OnError((flurlCall) =>
{
_logger.LogError(flurlCall.Exception, flurlCall.Exception.Message);
})
.PostAsync(httpContent));
and the Retry Policy:
public AsyncRetryPolicy StatusAsyncRetryPolicy() => Policy
.Handle<FlurlHttpException>(RetryPolicyHelpers.IsTransientError)
.WaitAndRetryAsync(5, retryAttempt =>
{
var nextAttemptIn = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt));
_logger.LogWarning($"StatusRetryPolicy: Retry attempt {retryAttempt} to make request. Next try in {nextAttemptIn.TotalSeconds} seconds.");
return nextAttemptIn;
});
I have several suggestions for your code.
!File.Exists
I assume the filePath
is provided by the user. If that file does not exist then short-cutting the execution and saying everything went fine << seems to me lazy error handling.
Early exiting / failing fast is a good pattern but please let the caller of your method know that some pre-condition(s) failed prior any real work has been done.
"application/zip"
Since you haven't shared with us all relevant code that's why it is hard to tell, but please make sure that the filePath
is pointing to a real zip compressed file (not just checking that the file's extension is .zip
).
return ....ExecuteAsync
As it was stated by dropoutcoder you should await
the ExecuteAsync
to be sure that method-scoped httpContent
is not disposed.
.PostAsync(httpContent))
Most of the time those endpoints that can be called with POST
verb are not implemented in an idempotent way. In other words it might happen that without request de-duplication you create duplicates or even worse with retries.
Please make sure that retry logic is applicable in your use case.
WithOAuthBearerToken(accessToken)
Creating a retry logic to refresh the accessToken
if it is expired seems to me a more reasonable choice for a retry logic. Here you can find two alternative implementations how to achieve that.
WithTimeout(TimeSpan.FromMinutes(timeOutMinutes))
Because you are mixing Flurl resilience features with Polly resilience features it is pretty hard to tell without reading the documentation (or experimenting) that this timeout is for all retry attempts (global) or for each attempt (local).
I would suggest to use Polly's Timeout
with Policy.Wrap
to define an overarching timeout constraint or a per request timeout limit.
_logger.LogWarning
Last but not least I would suggest to use WaitAndRetryAsync
in the way that you perform the logging inside the onRetry
delegate
.WaitAndRetryAsync(5,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry:(ex, nextAttemptIn, retryAttempt) =>
_logger.LogWarning($"StatusRetryPolicy: Retry attempt {retryAttempt} to make request. Next try in {nextAttemptIn.TotalSeconds} seconds."));
Here you can access the exception which triggers a new retry attempt.