Retrying failed requests and timeouts C # HttpClient

I'm trying to retry in HttpClient

DelegatingHandler

, so that responses like 403 Server Unavailable

and timeouts are treated as transient failures and automatically retry.

I started with the code http://blog.devscrum.net/2014/05/building-a-transient-retry-handler-for-the-net-httpclient/ , which works for the case 403 Server Unavailable

, but does not treat timeouts as transient glitches. However, I like the general idea of ​​using the Microsoft Transient Fault Transaction Block to handle the redo logic.

Here is my current code. It uses its own subclass Exception

:

public class HttpRequestExceptionWithStatus : HttpRequestException {
    public HttpRequestExceptionWithStatus(string message) : base(message)
    {
    }
    public HttpRequestExceptionWithStatus(string message, Exception inner) : base(message, inner)
    {
    }
    public HttpStatusCode StatusCode { get; set; }
    public int CurrentRetryCount { get; set; }
}

      

And here is the class of transient defects:

public class HttpTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy {
    public bool IsTransient(Exception ex)
    {
        var cex = ex as HttpRequestExceptionWithStatus;
        var isTransient = cex != null && (cex.StatusCode == HttpStatusCode.ServiceUnavailable
                          || cex.StatusCode == HttpStatusCode.BadGateway
                          || cex.StatusCode == HttpStatusCode.GatewayTimeout);
        return isTransient;
    }
}

      

The idea is that timeouts should be turned into exceptions ServiceUnavailable

, as if the server returned this HTTP error code. Here is the subclass DelegatingHandler

:

public class RetryDelegatingHandler : DelegatingHandler {
    public const int RetryCount = 3;

    public RetryPolicy RetryPolicy { get; set; }

    public RetryDelegatingHandler(HttpMessageHandler innerHandler) : base(innerHandler)
    {
        RetryPolicy = new RetryPolicy(new HttpTransientErrorDetectionStrategy(), new ExponentialBackoff(retryCount: RetryCount,
            minBackoff: TimeSpan.FromSeconds(1), maxBackoff: TimeSpan.FromSeconds(10), deltaBackoff: TimeSpan.FromSeconds(5)));
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var responseMessage = (HttpResponseMessage)null;
        var currentRetryCount = 0;

        EventHandler<RetryingEventArgs> handler = (sender, e) => currentRetryCount = e.CurrentRetryCount;
        RetryPolicy.Retrying += handler;

        try {
            await RetryPolicy.ExecuteAsync(async () => {
                try {
                    App.Log("Sending (" + currentRetryCount + ") " + request.RequestUri +
                        " content " + await request.Content.ReadAsStringAsync());
                    responseMessage = await base.SendAsync(request, cancellationToken);
                } catch (Exception ex) {
                    var wex = ex as WebException;
                    if (cancellationToken.IsCancellationRequested || (wex != null && wex.Status == WebExceptionStatus.UnknownError)) {
                        App.Log("Timed out waiting for " + request.RequestUri + ", throwing exception.");
                        throw new HttpRequestExceptionWithStatus("Timed out or disconnected", ex) {
                            StatusCode = HttpStatusCode.ServiceUnavailable,
                            CurrentRetryCount = currentRetryCount,
                        };
                    }

                    App.Log("ERROR awaiting send of " + request.RequestUri + "\n- " + ex.Message + ex.StackTrace);
                    throw;
                }
                if ((int)responseMessage.StatusCode >= 500) {
                    throw new HttpRequestExceptionWithStatus("Server error " + responseMessage.StatusCode) {
                        StatusCode = responseMessage.StatusCode,
                        CurrentRetryCount = currentRetryCount,
                    };
                }
                return responseMessage;
            }, cancellationToken);

            return responseMessage;
        } catch (HttpRequestExceptionWithStatus ex) {
            App.Log("Caught HREWS outside Retry section: " + ex.Message + ex.StackTrace);
            if (ex.CurrentRetryCount >= RetryCount) {
                App.Log(ex.Message);
            }
            if (responseMessage != null) return responseMessage;
            throw;
        } catch (Exception ex) {
            App.Log(ex.Message + ex.StackTrace);
            if (responseMessage != null) return responseMessage;
            throw;
        } finally {
            RetryPolicy.Retrying -= handler;
        }
    }
}

      

The problem is that after the first timeout, subsequent retries are immediately disabled because everyone has a cancellation token. But if I create a new one CancellationTokenSource

and use its token, no timeouts ever happen because I don't have access to the original original departure token HttpClient

.

I was thinking about subclassing HttpClient

and overriding SendAsync

, but its main overload is not virtual. I could potentially just create a new function unnamed SendAsync

, but then it is not a replacement for a replacement, and I would have to replace all cases of things like GetAsync

.

Any other ideas?

+3


source to share


1 answer


You may just want to subclass (or wrap) HttpClient

; it seems cleaner to me to repeat requests HttpClient

at the handler level rather than at the handler level. If this is not acceptable, you need to separate the "timeout" values.

Since your handler is actually executing multiple results in one, it HttpClient.Timeout

applies to the entire process, including repetitions. You can add another timeout value to your handler, which will timeout for each request, and use that with the associated cancellation token source:



public class RetryDelegatingHandler : DelegatingHandler {
  public TimmeSpan PerRequestTimeout { get; set; }
  ...
  protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  {
    var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
    cts.CancelAfter(PerRequestTimeout);
    var token = cts.Token;
    ...
        responseMessage = await base.SendAsync(request, token);
    ...
  }
}

      

+6


source







All Articles