You can basically early exit with a fallback policy
IAsyncPolicy<HttpResponseMessage> GetEarlyExitPolicy()
=> Policy<HttpResponseMessage>
.HandleResult(x => x.StatusCode is HttpStatusCode.Forbidden)
.FallbackAsync(_ => Task.FromResult(new HttpResponseMessage((HttpStatusCode)0));
So, whenever you receive a Forbidden then you change the original result with a bogus one. The status code is intentionally out of the normal range to ease the detection.
Here is a working example:
public class Program
{
public static async Task Main()
{
var x = Policy.WrapAsync(GetBaselinePolicy(), GetEarlyExitPolicy());
var res = await x.ExecuteAsync(() => Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden)));
res.Dump();
}
static IAsyncPolicy<HttpResponseMessage> GetBaselinePolicy() {
return Policy<HttpResponseMessage>
.HandleResult(x => x.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.Gone)
.WaitAndRetryForeverAsync(_ => { "retry".Dump(); return TimeSpan.FromSeconds(1);});
}
static IAsyncPolicy<HttpResponseMessage> GetEarlyExitPolicy() {
return Policy<HttpResponseMessage>
.HandleResult(x => x.StatusCode is HttpStatusCode.Forbidden)
.FallbackAsync(_ => { "fallback".Dump(); return Task.FromResult(new HttpResponseMessage((HttpStatusCode)0)); });
}
}
UPDATE #1
The caveat here is of course that the response remains modified, and your caller sees status code 0 in the end. That wouldn't be ideal I think - I'd strongly refer responses that should be excluded from the retry policy to appear downstream in their original form. Eg in my case, while I may not want to retry certain kind of errors, I'd still want to log (or otherwise special case) them further downstream. Downstream should ideally not even be aware polly exists somewhere upstream.
In order to keep the original response and early exit in case of Forbidden response you could use Polly.Context. The fallback policy has several overloads. One of them allows you access the original response / exception and the context. So, we can save the outcome of the operation into the context and after the ExecuteAsync call we can examine it.
To keep all this logic hidden from the downstream you can simply wrap the code inside a DelegatingHandler:
public class YourHttpClientHandler : DelegatingHandler
{
private const string ContextKey = "EarlyExitedResult";
private const HttpStatusCode EarlyExitCode = (HttpStatusCode)0;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var context = new Polly.Context();
var strategy = Policy.WrapAsync(GetBaselinePolicy(), GetEarlyExitPolicy());
var pollyResult = await strategy.ExecuteAsync(ctx => base.SendAsync(request, cancellationToken), context);
if (pollyResult.StatusCode == EarlyExitCode)
{
var result = (DelegateResult<HttpResponseMessage>)context[ContextKey];
if (result.Result is not null)
return result.Result;
throw result.Exception;
}
return pollyResult;
}
static IAsyncPolicy<HttpResponseMessage> GetBaselinePolicy()
{
return Policy<HttpResponseMessage>
.HandleResult(x => x.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.Gone)
.WaitAndRetryForeverAsync(_ => { "retry".Dump(); return TimeSpan.FromSeconds(1);});
}
static IAsyncPolicy<HttpResponseMessage> GetEarlyExitPolicy()
{
return Policy<HttpResponseMessage>
.HandleResult(x => x.StatusCode is HttpStatusCode.Forbidden)
.FallbackAsync((res, ctx, ct) =>
{
ctx[ContextKey] = res;
return Task.FromResult(new HttpResponseMessage(EarlyExitCode));
}, onFallbackAsync: (res, cxt) => Task.CompletedTask);
}
}
- The
GetBaselinePolicy is your external retry policy
- The
GetEarlyExitPolicy is your fallback policy to capture the outcome in case of Forbidden status code.
- The original response is saved into the context
- The
SendAsync combines the above two policies, executes the original request with the combined policy, and finally examines the outcome
- If the status code is 0 then you fetch the outcome from the context and act accordingly
- If the status code is other than 0 then you return that code
The usage of this DelegatingHandler is straightforward:
public class Program
{
public static async Task Main()
{
ServiceCollection services = new();
services.AddTransient<YourHttpClientHandler>();
services.AddHttpClient(string.Empty)
.AddHttpMessageHandler<YourHttpClientHandler>();
var provider = services.BuildServiceProvider();
var httpClientFactory = provider.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient();
//var res = await httpClient.GetAsync("https://httpstat.us/410");
var res = await httpClient.GetAsync("https://httpstat.us/403");
res.Dump();
}
}
Here you can give it a try on dotnetfiddle: https://dotnetfiddle.net/3Zbs53