web-dev-qa-db-fra.com

Réessayer les demandes infructueuses de HttpClient

Je construis une fonction qui donne un objet HttpContent, émet une demande et réessaye en cas d'échec. Cependant, il y a des exceptions indiquant que l'objet HttpContent est supprimé après l'émission de la demande. Est-il possible de copier ou de dupliquer l'objet HttpContent afin que je puisse émettre plusieurs demandes?.

 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"));

(Évidemment je n'essaye pas indéfiniment mais le code ci-dessus est essentiellement ce que je veux).

Il donne cette 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)

Est-il possible de dupliquer un objet HttpContent ou de le réutiliser?

31
samirahmed

Au lieu d'implémenter la fonctionnalité de nouvelle tentative qui enveloppe la HttpClient, envisagez de construire la HttpClient avec une HttpMessageHandler qui exécute la logique de nouvelle tentative en interne. Par exemple:

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;
        }

        // ...
    }
}
60
Dan Bjorge

Réponse ASP.NET Core 2.1

ASP.NET Core 2.1 a ajouté le support pour Polly directement. Ici UnreliableEndpointCallerService est une classe qui accepte un HttpClient dans son constructeur. Les demandes ayant échoué réessayeront avec un recul exponentiel, de sorte que la prochaine tentative aura lieu dans un délai exponentiellement plus long après le précédent:

services
    .AddHttpClient<UnreliableEndpointCallerService>()
    .AddTransientHttpErrorPolicy(
        x => x.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)));

Pensez également à lire mon article de blog "Configuration optimale de HttpClientFactory" .

Réponse d'autres plateformes

Cette implémentation utilise Polly pour réessayer avec un recul exponentiel de sorte que la tentative suivante ait lieu exponentiellement plus longtemps après la précédente. Il tente également si une HttpRequestException ou TaskCanceledException est renvoyée en raison d'un délai d'attente. Polly est beaucoup plus facile à utiliser que 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");
}
29

Les réponses actuelles ne fonctionneront pas comme prévu dans tous les cas, en particulier dans le cas très courant du délai d'attente de la demande (voir mes commentaires ici). 

En outre, ils implémentent une stratégie de nouvelle tentative très naïve: souvent, vous voudriez quelque chose de plus sophistiqué, tel que le retrait exponentiel (valeur par défaut dans l'API Azure Storage Client). 

Je suis tombé par hasard sur TOPAZ en lisant un article lié au blog } _ (offrant également la nouvelle approche erronée interne). Voici ce que je suis venu avec:

// 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(); 
    });
}

Notez le paramètre délégué requester. Not doit être une HttpRequestMessage, car vous ne pouvez pas envoyer la même demande plusieurs fois. En ce qui concerne les stratégies, cela dépend de votre cas d'utilisation. Par exemple, une stratégie de détection d'erreur transitoire pourrait être aussi simple que:

private sealed class TransientErrorCatchAllStrategy : ITransientErrorDetectionStrategy
{
    public bool IsTransient(Exception ex)
    {
        return true;
    }
}

En ce qui concerne la stratégie de relance, TOPAZ propose trois options:

  1. FixedInterval
  2. Incrémentale
  3. ExponentialBackoff

Par exemple, voici l'équivalent TOPAZ de ce que la bibliothèque de stockage du client Azure utilise par défaut:

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);

Pour plus d'informations, voir http://msdn.Microsoft.com/en-us/library/hh680901(v=pandp.50).aspx

EDIT Notez que si votre requête contient un objet HttpContent, vous devrez le régénérer à chaque fois car cela sera également supprimé par HttpClient (merci d'avoir capturé Alexandre Pepin). Par exemple () => httpClient.PostAsync(url, new StringContent("foo"))).

26
Ohad Schneider

La duplication du StringContent n'est probablement pas la meilleure idée. Mais une simple modification pourrait résoudre le problème. Modifiez simplement la fonction et créez l'objet StringContent à l'intérieur de la boucle, quelque chose comme:

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;
} 

et ensuite l'appeler 

ExecuteWithRetry("http://www.requestb.in/xfxcva" /*valid url*/, "Hello World");
14
VladL

Je l'ai essayé et j'ai travaillé en utilisant des tests unitaires et d'intégration. Cependant, cela a bloqué lorsque j'ai appelé à partir de l'URL REST J'ai trouvé cet article intéressant qui explique pourquoi il reste bloqué sur cette ligne.

response = await base.SendAsync(request, cancellationToken);

La solution à cela est que vous avez ajouté .ConfigureAwait(false) à la fin.

response = await base.SendAsync(request, token).ConfigureAwait(false);

J'ai également ajouté créer une partie liée de jeton là-bas comme ceci.

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;
0
Pranav Patel

Avec RestEase And Task, lors d’une nouvelle tentative avec httpClient réutilisé dans de nombreux appels (singleton), il frezze et lève TaskCanceledException. Pour résoudre ce problème, il faut éliminer la réponse avant de réessayer.

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;
    }
}
0
Pascal Martin

j'ai presque le même problème. La bibliothèque de mise en file d'attente HttpWebRequest, qui garantit la livraison de la demande Je viens de mettre à jour (voir EDIT3) mon approche pour éviter les plantages, mais j’ai encore besoin d’un mécanisme général pour garantir la livraison du message (ou sa remise à nouveau au cas où le message non livrés).

0
Vitalii Vasylenko