问题
I am building a function that given an HttpContent Object, will issues request and retry on failure. However I get exceptions saying that HttpContent Object is disposed after issuing the request. Is there anyway to copy or duplicate the HttpContent Object so that I can issue multiple requests.
public HttpResponseMessage ExecuteWithRetry(string url, HttpContent content)
{
HttpResponseMessage result = null;
bool success = false;
do
{
using (var client = new HttpClient())
{
result = client.PostAsync(url, content).Result;
success = result.IsSuccessStatusCode;
}
}
while (!success);
return result;
}
// Works with no exception if first request is successful
ExecuteWithRetry("http://www.requestb.in/xfxcva" /*valid url*/, new StringContent("Hello World"));
// Throws if request has to be retried ...
ExecuteWithRetry("http://www.requestb.in/badurl" /*invalid url*/, new StringContent("Hello World"));
(Obviously I don't try indefinitely but the code above is essentially what i want).
It yields this exception
System.AggregateException: One or more errors occurred. ---> System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'System.Net.Http.StringContent'.
at System.Net.Http.HttpContent.CheckDisposed()
at System.Net.Http.HttpContent.CopyToAsync(Stream stream, TransportContext context)
at System.Net.Http.HttpClientHandler.GetRequestStreamCallback(IAsyncResult ar)
--- End of inner exception stack trace ---
at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
at System.Threading.Tasks.Task`1.get_Result()
at Submission#8.ExecuteWithRetry(String url, HttpContent content)
Is there anyway to duplicate an HttpContent Object or reuse it?
回答1:
Instead of implementing retry functionality that wraps the HttpClient
, consider constructing the HttpClient
with a HttpMessageHandler
that performs the retry logic internally. For example:
public class RetryHandler : DelegatingHandler
{
// Strongly consider limiting the number of retries - "retry forever" is
// probably not the most user friendly way you could respond to "the
// network cable got pulled out."
private const int MaxRetries = 3;
public RetryHandler(HttpMessageHandler innerHandler)
: base(innerHandler)
{ }
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
HttpResponseMessage response = null;
for (int i = 0; i < MaxRetries; i++)
{
response = await base.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode) {
return response;
}
}
return response;
}
}
public class BusinessLogic
{
public void FetchSomeThingsSynchronously()
{
// ...
// Consider abstracting this construction to a factory or IoC container
using (var client = new HttpClient(new RetryHandler(new HttpClientHandler())))
{
myResult = client.PostAsync(yourUri, yourHttpContent).Result;
}
// ...
}
}
回答2:
ASP.NET Core 2.1 Answer
ASP.NET Core 2.1 added support for Polly directly. Here UnreliableEndpointCallerService
is a class which accepts a HttpClient
in its constructor. Failed requests will retry with an exponential back-off so that the next retry takes place in an exponentially longer time after the previous one:
services
.AddHttpClient<UnreliableEndpointCallerService>()
.AddTransientHttpErrorPolicy(
x => x.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)));
Also, consider reading my blog post "Optimally Configuring HttpClientFactory".
Other Platforms Answer
This implementation uses Polly to retry with an exponential back-off so that the next retry takes place in an exponentially longer time after the previous one. It also retries if a HttpRequestException
or TaskCanceledException
is thrown due to a timeout. Polly is much easier to use than Topaz.
public class HttpRetryMessageHandler : DelegatingHandler
{
public HttpRetryMessageHandler(HttpClientHandler handler) : base(handler) {}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken) =>
Policy
.Handle<HttpRequestException>()
.Or<TaskCanceledException>()
.OrResult<HttpResponseMessage>(x => !x.IsSuccessStatusCode)
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)))
.ExecuteAsync(() => base.SendAsync(request, cancellationToken));
}
using (var client = new HttpClient(new HttpRetryMessageHandler(new HttpClientHandler())))
{
var result = await client.GetAsync("http://example.com");
}
回答3:
The current answers won't work as expected in all cases, specifically in the very common case of request timeout (see my comments there).
In addition, they implement a very naive retry strategy - many times you'd want something a bit more sophosticated, such as exponential backoff (which is the default in the Azure Storage Client API).
I stumbled upon TOPAZ while reading a related blog post (also offering the misguided internal retry approach). Here's what I came up with:
// sample usage: var response = await RequestAsync(() => httpClient.GetAsync(url));
Task<HttpResponseMessage> RequestAsync(Func<Task<HttpResponseMessage>> requester)
{
var retryPolicy = new RetryPolicy(transientErrorDetectionStrategy, retryStrategy);
//you can subscribe to the RetryPolicy.Retrying event here to be notified
//of retry attempts (e.g. for logging purposes)
return retryPolicy.ExecuteAsync(async () =>
{
HttpResponseMessage response;
try
{
response = await requester().ConfigureAwait(false);
}
catch (TaskCanceledException e) //HttpClient throws this on timeout
{
//we need to convert it to a different exception
//otherwise ExecuteAsync will think we requested cancellation
throw new HttpRequestException("Request timed out", e);
}
//assuming you treat an unsuccessful status code as an error
//otherwise just return the respone here
return response.EnsureSuccessStatusCode();
});
}
Note the requester
delegate parameter. It should not be an HttpRequestMessage
since you can't send the same request multiple times. As for the strategies, that depends on your use case. For example, a transient error detection strategy could be as simple as:
private sealed class TransientErrorCatchAllStrategy : ITransientErrorDetectionStrategy
{
public bool IsTransient(Exception ex)
{
return true;
}
}
As for the retry strategy, TOPAZ offers three options:
- FixedInterval
- Incremental
- ExponentialBackoff
For example, here's the TOPAZ equivalent of what the Azure Client Storage Library uses for default:
int retries = 3;
var minBackoff = TimeSpan.FromSeconds(3.0);
var maxBackoff = TimeSpan.FromSeconds(120.0);
var deltaBackoff= TimeSpan.FromSeconds(4.0);
var strategy = new ExponentialBackoff(retries, minBackoff, maxBackoff, deltaBackoff);
For more information see http://msdn.microsoft.com/en-us/library/hh680901(v=pandp.50).aspx
EDIT Note that if your request contains an HttpContent
object, you'll have to regenerate it every time as that will be disposed by HttpClient
as well (thanks for catching that Alexandre Pepin). For example () => httpClient.PostAsync(url, new StringContent("foo")))
.
回答4:
Duplicating the StringContent isn't probably the best idea. But simple modification could fix the problem. Just modify the function and create the StringContent object inside of the loop, something like:
public HttpResponseMessage ExecuteWithRetry(string url, string contentString)
{
HttpResponseMessage result = null;
bool success = false;
using (var client = new HttpClient())
{
do
{
result = client.PostAsync(url, new StringContent(contentString)).Result;
success = result.IsSuccessStatusCode;
}
while (!success);
}
return result;
}
and then call it
ExecuteWithRetry("http://www.requestb.in/xfxcva" /*valid url*/, "Hello World");
回答5:
This builds off the accepted answer but adds the ability to pass in the amount of retries, plus adds the ability to add non-blocking delays/ wait time to each request. It also uses a try catch to ensure the retry continues to happen after an exception has occurred. And last, I added code to break out of the loop in the case of BadRequests, you don't want to resend the same bad request multiple times.
public class HttpRetryHandler : DelegatingHandler
{
private int MaxRetries;
private int WaitTime;
public HttpRetryHandler(HttpMessageHandler innerHandler, int maxRetries = 3, int waitSeconds = 0)
: base(innerHandler)
{
MaxRetries = maxRetries;
WaitTime = waitSeconds * 1000;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage response = null;
for (int i = 0; i < MaxRetries; i++)
{
try
{
response = await base.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode)
{
return response;
}
else if(response.StatusCode == HttpStatusCode.BadRequest)
{
// Don't reattempt a bad request
break;
}
}
catch
{
// Ignore Error As We Will Attempt Again
}
finally
{
response.Dispose();
}
if(WaitTime > 0)
{
await Task.Delay(WaitTime);
}
}
return response;
}
}
}
回答6:
i have almost the same issue. HttpWebRequest queueing library, which guarantees request delivery I just updated (see EDIT3) my approach to avoid crashes, but i still need general mechanism to guarantee message delivery (or re-delivery in case message was not delivered).
回答7:
I tried it and worked while using unit and integration tests. However, it stuck when I actually called from REST URL. I found this interesting post which explains why it gets stuck at this line.
response = await base.SendAsync(request, cancellationToken);
The fix to this is that you have .ConfigureAwait(false)
added at the end.
response = await base.SendAsync(request, token).ConfigureAwait(false);
I also added create linked token part there like this.
var linkedToken = cancellationToken.CreateLinkedSource();
linkedToken.CancelAfter(new TimeSpan(0, 0, 5, 0));
var token = linkedToken.Token;
HttpResponseMessage response = null;
for (int i = 0; i < MaxRetries; i++)
{
response = await base.SendAsync(request, token).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
return response;
}
}
return response;
回答8:
With RestEase And Task, on retry with httpClient reused in many call (singleton) it frezze and throw TaskCanceledException. To fix this whe need to Dispose() the failed response before retry
public class RetryHandler : DelegatingHandler
{
// Strongly consider limiting the number of retries - "retry forever" is
// probably not the most user friendly way you could respond to "the
// network cable got pulled out."
private const int MaxRetries = 3;
public RetryHandler(HttpMessageHandler innerHandler)
: base(innerHandler)
{ }
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
HttpResponseMessage response = null;
for (int i = 0; i < MaxRetries; i++)
{
response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (response.IsSuccessStatusCode) {
return response;
}
response.Dispose();
}
return response;
}
}
回答9:
//Could retry say 5 times
HttpResponseMessage response;
int numberOfRetry = 0;
using (var httpClient = new HttpClient())
{
do
{
response = await httpClient.PostAsync(uri, content);
numberOfRetry++;
} while (response.IsSuccessStatusCode == false | numberOfRetry < 5);
}
return response;
.........
回答10:
You also refer to Building a Transient Retry Handler for the .NET HttpClient. Visit refer to KARTHIKEYAN VIJAYAKUMAR post.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data.SqlClient;
using System.Net.Http;
using System.Threading;
using System.Diagnostics;
using System.Net;
using Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling;
namespace HttpClientRetyDemo
{
class Program
{
static void Main(string[] args)
{
var url = "http://RestfulUrl";
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
var handler = new RetryDelegatingHandler
{
UseDefaultCredentials = true,
PreAuthenticate = true,
Proxy = null
};
HttpClient client = new HttpClient(handler);
var result = client.SendAsync(httpRequestMessage).Result.Content
.ReadAsStringAsync().Result;
Console.WriteLine(result.ToString());
Console.ReadKey();
}
}
/// <summary>
/// Retry Policy = Error Detection Strategy + Retry Strategy
/// </summary>
public static class CustomRetryPolicy
{
public static RetryPolicy MakeHttpRetryPolicy()
{
// The transient fault application block provides three retry policies
// that you can use. These are:
return new RetryPolicy(strategy, exponentialBackoff);
}
}
/// <summary>
/// This class is responsible for deciding whether the response was an intermittent
/// transient error or not.
/// </summary>
public class HttpTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy
{
public bool IsTransient(Exception ex)
{
if (ex != null)
{
HttpRequestExceptionWithStatus httpException;
if ((httpException = ex as HttpRequestExceptionWithStatus) != null)
{
if (httpException.StatusCode == HttpStatusCode.ServiceUnavailable)
{
return true;
}
else if (httpException.StatusCode == HttpStatusCode.MethodNotAllowed)
{
return true;
}
return false;
}
}
return false;
}
}
/// <summary>
/// The retry handler logic is implementing within a Delegating Handler. This has a
/// number of advantages.
/// An instance of the HttpClient can be initialized with a delegating handler making
/// it super easy to add into the request pipeline.
/// It also allows you to apply your own custom logic before the HttpClient sends the
/// request, and after it receives the response.
/// Therefore it provides a perfect mechanism to wrap requests made by the HttpClient
/// with our own custom retry logic.
/// </summary>
class RetryDelegatingHandler : HttpClientHandler
{
public RetryPolicy retryPolicy { get; set; }
public RetryDelegatingHandler()
: base()
{
retryPolicy = CustomRetryPolicy.MakeHttpRetryPolicy();
}
protected async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
HttpResponseMessage responseMessage = null;
var currentRetryCount = 0;
//On Retry => increments the retry count
retryPolicy.Retrying += (sender, args) =>
{
currentRetryCount = args.CurrentRetryCount;
};
try
{
await retryPolicy.ExecuteAsync(async () =>
{
responseMessage = await base.SendAsync(request, cancellationToken)
.ConfigureAwait(false);
if ((int)responseMessage.StatusCode > 500)
{
// When it fails after the retries, it would throw the exception
throw new HttpRequestExceptionWithStatus(
string.Format("Response status code {0} indicates server error",
(int)responseMessage.StatusCode))
{
StatusCode = responseMessage.StatusCode,
CurrentRetryCount = currentRetryCount
};
}// returns the response to the main method(from the anonymous method)
return responseMessage;
}, cancellationToken).ConfigureAwait(false);
return responseMessage;// returns from the main method => SendAsync
}
catch (HttpRequestExceptionWithStatus exception)
{
if (exception.CurrentRetryCount >= 3)
{
//write to log
}
if (responseMessage != null)
{
return responseMessage;
}
throw;
}
catch (Exception)
{
if (responseMessage != null)
{
return responseMessage;
}
throw;
}
}
}
/// <summary>
/// Custom HttpRequestException to allow include additional properties on my exception,
/// which can be used to help determine whether the exception is a transient
/// error or not.
/// </summary>
public class HttpRequestExceptionWithStatus : HttpRequestException
{
public HttpStatusCode StatusCode { get; set; }
public int CurrentRetryCount { get; set; }
public HttpRequestExceptionWithStatus()
: base() { }
public HttpRequestExceptionWithStatus(string message)
: base(message) { }
public HttpRequestExceptionWithStatus(string message, Exception inner)
: base(message, inner) { }
}
}
来源:https://stackoverflow.com/questions/19260060/retrying-httpclient-unsuccessful-requests