twitchax / AspNetCore.Proxy

ASP.NET Core Proxies made easy.
MIT License
524 stars 83 forks source link

Retry proxied request #84

Open joaocpribeiro opened 2 years ago

joaocpribeiro commented 2 years ago

Hi, I am trying to proxy some requests with a different authorization header (bearer token). Now I would like to retry the request if the response is 401, so that I could use refresh the token first. Also I would like to retry only once. Is there a way to do it?

Note: For now I am defining the proxy on the controller.

joaocpribeiro commented 2 years ago

I found a solution by myself after discussing with colleagues. Calling the proxy again from the WithAfterReceive function seems to work.

twitchax commented 2 years ago

Hmmmm. Glad you found a solution, but retry logic might be interesting to add.

joaocpribeiro commented 2 years ago

Sorry for commenting here again after one week, but it looks like I did not test as much as I should. My code to test the retry was something like the following (changing status codes for testing purposes):

private HttpProxyOptions GetHttpProxyOptions(bool refreshTokenIfUnauthorized = true)
{
    var httpProxyOptions = HttpProxyOptionsBuilder.Instance
        .WithAfterReceive(async (c, hrm) =>
        {
            if (!refreshTokenIfUnauthorized)
            {
                hrm.StatusCode = HttpStatusCode.Forbidden;
                return;
            }

            if (hrm.StatusCode == HttpStatusCode.OK) hrm.StatusCode = HttpStatusCode.NotAcceptable;
            await this.HttpProxyAsync(hrm.RequestMessage.RequestUri.AbsoluteUri, GetHttpProxyOptions(refreshTokenIfUnauthorized: false));
        })
        .Build();
    return httpProxyOptions;
}

And the main call in the controller was:

public async Task Test()
{
    await this.HttpProxyAsync(testUrl, GetHttpProxyOptions());
}

As expected, this Test action is responding with 403 (Forbidden)... but only when the response has body. When the response has no body, the status code in the response is 406 (NotAcceptable). In both cases (with and without body) the 'WithAfterReceive' is reached twice, as expected. This is quite strange, and after I briefly check the code I did not find any reason for this to happen. Another finding is that, if I read the response body - even though I don't need it for anything - (adapted code following), I always get the desired 403 response.

            if (!refreshTokenIfUnauthorized)
            {
                hrm.StatusCode = HttpStatusCode.Forbidden;
                await hrm.Content.ReadAsStringAsync();
                return;
            }

Even though I found a way to obtain the desired behavior, I would like to avoid this hack. :) Any thoughts?

twitchax commented 2 years ago

How would you feel about a WithRetryIf handler?

joaocpribeiro commented 2 years ago

@twitchax It sounds great to me. Important (IMO) would be the following acceptance criteria:

Are you thinking about bringing up this feature? Do you have an estimation when it would be ready? Thanks in advance!

h82258652 commented 2 years ago

I think we can do this with Polly. Install Microsoft.Extensions.Http.Polly nuget package to your project.

public void ConfigureServices(IServiceCollection services)
{
-    services.AddProxies();

+    services.AddRouting();
+    services
+        .AddHttpClient("AspNetCore.Proxy.HttpProxyClient")// due to Helpers.HttpProxyClientName is internel.
+        .AddTransientHttpErrorPolicy(policy => policy
+            .OrResult(response => response.StatusCode == HttpStatusCode.NotFound)
+            .RetryAsync(3));// if response failed or not found then retry 3 times.
}
joaocpribeiro commented 2 years ago

@h82258652 Sorry for the delayed response. Maybe it works, but I am struggling with an operation that I need to do between attempts. I need to access the user's claims. I noticed the RetryAsync has an onRetry action as parameter, but I don't find a way to access the claims from there.