0

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?

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Howiecamp
  • 2,981
  • 6
  • 38
  • 59
  • You can create your own class inheriting from Policy and implement your own logic of failure there.. And use that policy class to wrap your web api call under Polly. – Chetan Aug 06 '18 at 01:04
  • https://github.com/App-vNext/Polly#step-1b-optionally-specify-return-results-you-want-to-handle Check this link in the docs, think it describes what you're trying to do. – Max Hampton Aug 06 '18 at 01:45
  • @Max I'm reading about it now- I'm going to give it a try in some code but it's not clear to me that I can do 1b (where the link parts) with out 1... – Howiecamp Aug 06 '18 at 02:27
  • @Howiecamp can you show code for your policy configuration? – Max Hampton Aug 06 '18 at 03:26
  • @Howiecamp Polly has `.HandleResult(...)` and you can use this directly, with first having to use `.Handle(...)`. However, if the result condition depends on retrieving the response with an additional _async_ call, that is not covered by a single Polly policy. See [this question and answer](https://stackoverflow.com/a/50844695/) - does this also cover your question? – mountain traveller Aug 06 '18 at 12:49
  • @mountaintraveller Great point you make. In my case it doesn't depend on the add'l async call (by the way what if it was an additional sync call? Would it still be an issue?), it's just a single call; please see my question which I significantly reworded and made more explicit. – Howiecamp Aug 06 '18 at 16:31
  • @MaxHampton I haven't got one yet, I'm struggling to put it together. – Howiecamp Aug 06 '18 at 16:32
  • @Howiecamp As long as you use one policy per `await`-ed call (as discussed in [that question and answer](https://stackoverflow.com/a/50844695/)), rather than try to introduce any `await` into (into the inside of) the `HandleResult(...)` clause, then certainly you can do it. I think the [answer here](https://github.com/App-vNext/Polly/issues/329#issuecomment-338319155) gives an exact answer to your case, but let me know if not! – mountain traveller Aug 06 '18 at 20:49

2 Answers2

0

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.

Max Hampton
  • 1,254
  • 9
  • 20
  • My understanding is that if I have a `Policy` then the filter expression (predicate) in my `Policy.HandleResult(Func filter)` must check some property off the `HttpResponseMessage` object that's returned, is that right? In my case I've got arbitrary text in the body of the response. – Howiecamp Aug 06 '18 at 20:56
  • Second question - you said, "You're configuring a policy to guard the HttpClient GetAsync method..." For sake of argument let's say I exclude that part from the equation, in other words I either handle or with Polly or not, whatever. In that case I'd guard against the `var content = await response.Content.ReadAsStringAsync();` call instead correct?? – Howiecamp Aug 06 '18 at 22:26
  • Success... I reoriented my thinking - I see where I was wrong. I took the approach of it's ok to have 2 policies - one for the httprequest itself and one to check the string contents of the returned data. So for the string contents (I expect presence or absence of a given string), I did: `var policy = Policy.HandleResult(r => r == "bad response").Wait...` followed by: `var body = await readAsStringAsyncPolicy.ExecuteAsync(() => response.Content.ReadAsStringAsync());` and boom I'm good. Do you think my approach is proper? – Howiecamp Aug 06 '18 at 22:50
  • I think your 2 policy approach is definitely proper. Whether you do that in series or wrapped is more of a design decision. I'll edit and add some pseudocode for how I might approach this. – Max Hampton Aug 06 '18 at 23:00
  • Great I'd love to see it. By the way I was wrong about my success - I was *still* thinking about this incorrectly. Kinda. Since I've got the ReadAsStringAsync operation wrapped, then when it fails what I need to do is re-execute the *original* policy that wraps the http call. So now I've got some thinking to do again as I'm not clear how to do that. – Howiecamp Aug 06 '18 at 23:06
  • Actually per my previous comment, can help? I'm struggling here. – Howiecamp Aug 17 '18 at 01:18
0

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)
Peter Csala
  • 17,736
  • 16
  • 35
  • 75