FluentHttpClient icon indicating copy to clipboard operation
FluentHttpClient copied to clipboard

Modify request before retrying

Open Jericho opened this issue 4 years ago • 2 comments

Currently the FluentHttpClient allows us to handle scenarios where a request fails (presumably due to a transient problem) and waiting a brief moment allows us to dispatch the exact same request. An example is when you exceed the number of requests that an API allows you to make in a given period of time. In this scenario, you can typically reissue the exact same request after waiting a second or two. This is working quite well.

However, there are more advanced scenarios that I feel FluentHttpClient is not allowing us to handle. Let me offer an example to illustrate what I mean: let's say we issue a request with a "bearer" authentication token and the response is HTTP 401 due to the token being expired. I can write a custom IRetryConfig that will automatically refresh the token, the ShouldRetry in my IRetryConfig class will return true to indicate that I want the request to be retried but I have no way of replacing the expired token with the new token. Again, this is just an example. What I'm trying to illustrate is a scenario where I need the request to be slightly modified before being retried.

To kick-start the discussion about how to handle this kind of scenarios, I would like to propose adding an additional parameter to the ShouldRetry and/or GetDelay methods in the IRetry interface like so:

// Current signature:
bool ShouldRetry(HttpResponseMessage response);
TimeSpan GetDelay(int retry, HttpResponseMessage response);

// What I propose:
bool ShouldRetry(HttpResponseMessage response, IRequest request);
TimeSpan GetDelay(int retry, HttpResponseMessage response, IRequest request);

This would allow developers to modify the request in their implementation of the ShouldRetry and/or GetDelay methods prior to the request being reissued.

If you think my suggestion makes sense, I'll be happy to submit a PR.

Jericho avatar Apr 06 '21 14:04 Jericho

Hi! Request coordinators are meant for scenarios like that. You can only have one request coordinator currently, but you can wrap the default one with custom logic. For example:

/// <summary>A coordinator that automatically updates the token if it expired, else applies retry policies.</summary>
public class AutoRefreshRetryCoordinator : IRequestCoordinator
{
    /// <summary>The default retry logic.</summary>
    private readonly IRequestCoordinator RetryCoordinator;

    /// <summary>Construct an instance.</summary>
    /// <param name="configs">The retry configurations to apply.</param>
    public AutoRefreshRetryCoordinator(IEnumerable<IRetryConfig?>? configs)
    {
        this.RetryCoordinator = new RetryCoordinator(configs);
    }

    /// <inheritdoc />
    public async Task<HttpResponseMessage> ExecuteAsync(IRequest request, Func<IRequest, Task<HttpResponseMessage>> dispatcher)
    {
        try
        {
            return await this.RetryCoordinator.ExecuteAsync(request, dispatcher);
        }
        catch (ApiException ex) when (ex.Status == HttpStatusCode.Unauthorized)
        {
            string newToken = ...;
            return await this.RetryCoordinator.ExecuteAsync(request.WithBearerAuthentication(newToken), dispatcher);
        }
    }
}

Pathoschild avatar Apr 07 '21 05:04 Pathoschild

I have a custom request coordinator for this purpose but I was thinking it would be simpler to allow RetryConfigs to modify the request.

Jericho avatar Apr 07 '21 12:04 Jericho