I'm trying to build in retrying in an HttpClient
DelegatingHandler
such that responses such as 503 Server Unavailable
and timeouts are treated as transient failures and retried automatically.
I was starting from the code at http://blog.devscrum.net/2014/05/building-a-transient-retry-handler-for-the-net-httpclient/ which works for the 403 Server Unavailable
case, but does not treat timeouts as transient failures. Still, I like the general idea of using the Microsoft Transient Fault Handling Block to handle the retry logic.
Here is my current code. It uses a custom Exception
subclass:
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 transient fault detector class:
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 ServiceUnavailable
exceptions as if the server had returned that HTTP error code. Here is the DelegatingHandler
subclass:
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 once the first timeout happens, the subsequent retries immediately time out because everything shares a cancellation token. But if I make a new CancellationTokenSource
and use its token, no timeouts ever happen because I don't have access to the original HttpClient
's cancellation token source.
I thought about subclassing HttpClient
and overriding SendAsync
but the main overload of it is not virtual. I could potentially just make a new function not called SendAsync
but then it's not a drop-in replacement and I'd have to replace all the cases of things like GetAsync
.
Any other ideas?