How do I use Polly for retries and transient fault handling of arbitrary "failure" conditions

2.1k Views Asked by At

I want to use Polly not to check for overt "failures" but rather for other conditions. Specifically, I want to make a single (async) call, for example httpClient.GetAsync(...) which for the purposes of this question I know will succeed - that is, after the execution of:

var response = await _myHttpRequestPolicy.ExecuteAsync(() => httpClient.GetAsync(uri));

response.IsSuccessStatusCode will be true.

Let's assume then I do the standard:

var content = await response.Content.ReadAsStringAsync();

and

content == { "Name":"Tom", "Age", 30", "ErrorCode":"12345" }

I want my policy logic to execute based on the contents (or absence or presence of) the ErrorCode portion of the response. So it's just a single call I'm making.

How can I do this with Polly?

2

There are 2 best solutions below

6
On

You're configuring a policy to guard the HttpClient GetAsync method, which returns Task<HttpResponseMessage>. You want to configure a Policy<HttpResponseMessage> to work with this method, using async handlers.

Policy<T>.HandleResult(Func<T, bool> filter) allows you to look at the HttpResponseMessage and determine whether you want to handle that result.

A couple of options. One, you could figure out deserializing/reading the HttpResponseMessage's json payload within the HandleResult method. You only get a Func<HttpResponseMessage, bool> to work with. This would need to happen synchronously, as adding async/await changes the return type to Task.

Second, you could apply the policy at a higher level. Get the response as you are httpclient.GetAsync(uri), then deserialize the content. Maybe have one Policy<HttpResponseMessage> wrap the httpclient call, and one Policy<MyAbstractApiResponse> to look for the custom error code after deserializing?

As a note, an API error should really be picked up by the IsSuccessStatusCode property on the HttpResponseMessage. Your REST api (is it yours? that's an assumption) should be setting status codes appropriate to the error, not solely 200's and custom response properties.

Related further reading: Check string content of response before retrying with Polly

Update:

class Consumer
{
  public void Test()
  {
    var apiResponse = Policy<IApiResponse>
      .HandleResult(resp => !string.IsNullOrWhiteSpace(resp.ErrorCode))
      // Setup Policy
      .ExecuteAsync(() => otherClassInstance.MakeApiCall());
  }
}

class OtherClass
{
  HttpClient httpClient = ...;
  public async Task<IApiResponse> MakeApiCall()
  {
    var response = Policy<HttpResponseMessage>
      .HandleResult(message => !message.IsSuccessStatusCode)
      // Setup Policy
      .ExecuteAsync(() => httpClient.GetAsync(url));

    var content = await response.ReadAsStringAsync();
    return new ApiResponse(content);
  }
}

I didn't look at real method names or syntax in putting that together so it might not compile. Hopefully you get the idea I'm trying to convey, one policy is called from within another.

0
On

As it was stated by mountain traveller the HandleResult was not designed to execute an async method in it. There are several workaround for this like:

  • using .GetAwaiter().GetResult() inside HandleResult
  • define two separate policies like it was in the linked github issue
  • or perform the async call inside the onRetryAsync and use a CancellationTokenSource to break the retry sequence

In this post let me focus on the last one

var noMoreRetrySignal = new CancellationTokenSource();

IAsyncPolicy<HttpResponseMessage> retryPolicy = Policy<HttpResponseMessage>
    .HandleResult(r => r.StatusCode == System.Net.HttpStatusCode.OK)
    .RetryAsync(3, onRetryAsync: async (dr, _, ctx) => {
        var content = await dr.Result.Content.ReadAsStringAsync();
        if (!content.Contains("ErrorCode")) //TODO: refine it based on your requirements
        {
            ctx["content"] = content;
            noMoreRetrySignal.Cancel();
        }
    });

var result = await retryPolicy.ExecuteAndCaptureAsync(
    async ct => await httpClient.GetAsync(address, ct),
    noMoreRetrySignal.Token);

if(result.FinalException is OperationCanceledException)
{
    Console.WriteLine(result.Context["content"]);
}
  • The noMoreRetrySignal is used to cancel the retry sequence
    • if there is no need for further retries
  • The retryPolicy checks whether the StatusCode is OK or not
  • The retryPolicy reads the response body asynchronously then performs some existence check
    • If it does not present then it stores the read string inside the Context and exits from the retry loop
    • If it presents then it continues the retry sequence
  • ExecuteAndCaptureAsync passes the noMoreRetrySignal's Token to the decorated method
    • The result of this call allows us to access the exception and the context
      • If the exception is caused by the CancellationTokenSource then we need to acces the context because the result.Result is null (that's why we had to use context to save the already read response body)