Runtime: HttpClient lève TaskCanceledException à l'expiration du délai

Créé le 25 mai 2017  ·  119Commentaires  ·  Source: dotnet/runtime

HttpClient lève une TaskCanceledException à l'expiration du délai dans certaines circonstances. Cela se produit pour nous lorsque le serveur est sous forte charge. Nous avons pu contourner le problème en augmentant le délai d'attente par défaut. Le fil de discussion MSDN suivant capture l'essence du problème rencontré : https://social.msdn.microsoft.com/Forums/en-US/d8d87789-0ac9-4294-84a0-91c9fa27e353/bug-in-httpclientgetasync-should-throw -webexception-not-taskcanceledexception?forum=netfxnetcom&prof=required

Merci

area-System.Net.Http enhancement

Commentaire le plus utile

@karelz ajoutant une note en tant qu'autre client impacté par cette exception trompeuse :)

Tous les 119 commentaires

@awithy quelle version de .NET Core utilisez-vous ?

1.1

Que ce soit la bonne conception ou non, par conception, les OperationCanceledExceptions sont levées pour les délais d'attente (et TaskCanceledException est une OperationCanceledException).
cc: @davidsh

Notre équipe a trouvé cela peu intuitif, mais cela semble fonctionner comme prévu. Malheureusement, lorsque nous avons atteint ce point, nous avons perdu un peu de temps à essayer de trouver une source d'annulation. Devoir gérer ce scénario différemment de l'annulation de tâche est assez moche (nous avons créé un gestionnaire personnalisé pour gérer cela). Cela semble également être une surutilisation de l'annulation en tant que concept.

Merci encore pour ta réponse rapide.

On dirait que de par sa conception + il n'y a eu que 2 plaintes de clients au cours des 6 derniers mois.
Fermeture.

@karelz ajoutant une note en tant qu'autre client impacté par cette exception trompeuse :)

Merci pour l'info, merci de voter pour le meilleur message (les problèmes peuvent être triés par celui-ci), merci !

Que ce soit la bonne conception ou non, par conception, les OperationCanceledExceptions sont levées pour les délais d'attente

C'est une mauvaise conception IMO. Il n'y a aucun moyen de savoir si la demande a été réellement annulée (c'est-à-dire que le jeton d'annulation transmis à SendAsync a été annulé) ou si le délai d'attente a expiré. Dans ce dernier cas, il serait logique de réessayer, alors que dans le premier cas, ce ne serait pas le cas. Il y a un TimeoutException qui serait parfait pour ce cas, pourquoi ne pas l'utiliser ?

Ouais, ça a aussi bouleversé notre équipe. Il y a tout un thread Stack Overflow dédié à essayer de le gérer: https://stackoverflow.com/questions/10547895/how-can-i-tell-when-httpclient-has-timed-out/32230327#32230327

J'ai publié une solution de contournement il y a quelques jours : https://www.thomaslevesque.com/2018/02/25/better-timeout-handling-with-httpclient/

Super merci.

Réouverture car l'intérêt est maintenant plus grand - le fil StackOverflow a maintenant 69 votes positifs.

cc @dotnet/ncl

Des nouvelles ? Se produit toujours dans dotnet core 2.0

Il figure sur notre liste des principaux problèmes pour l'après-2.1. Cela ne sera pas corrigé dans la version 2.0/2.1.

Nous avons plusieurs options que faire lorsque le délai d'attente se produit :

  1. Lancez TimeoutException au lieu de TaskCanceledException .

    • Avantage : délai d'expiration facile à distinguer de l'action d'annulation explicite au moment de l'exécution

    • Inconvénient : Modification technique avec rupture - un nouveau type d'exception est généré.

  2. Lancer TaskCanceledException avec une exception interne comme TimeoutException

    • Avantage : possibilité de distinguer le délai d'expiration de l'action d'annulation explicite au moment de l'exécution. Type d'exception compatible.

    • Question ouverte : Est-il acceptable de jeter la pile originale TaskCanceledException et d'en lancer une nouvelle, tout en préservant InnerException (comme TimeoutException.InnerException ) ? Ou devrions-nous conserver et simplement envelopper l'original TaskCanceledException ?



      • Soit : nouvelle TaskCanceledException > nouvelle TimeoutException > TaskCanceledException d'origine (avec InnerException d'origine qui peut être nulle)


      • Ou : new TaskCanceledException -> new TimeoutException -> original TaskCanceledException.InnerException (peut être nul)



  3. Lancer TaskCanceledException avec un message mentionnant le délai d'attente comme raison

    • Pour : Possibilité de distinguer le délai d'expiration de l'action d'annulation explicite à partir du message/des journaux

    • Inconvénient : ne peut pas être distingué lors de l'exécution, il s'agit de "débogage uniquement".

    • Question ouverte : Identique à [2] - devrions-nous envelopper ou remplacer l'original TaskCanceledException (et la pile)

Je penche vers l'option [2], en jetant la pile originale de TaskCanceledException d'origine.
@stephentoub @davidsh des pensées?

BTW : Le changement devrait être assez simple dans HttpClient.SendAsync où nous configurons le délai :
https://github.com/dotnet/corefx/blob/30fb78875148665b98748ede3013641734d9bf5c/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L433 -L461

L'exception de niveau supérieur doit toujours être une HttpRequestException. C'est le modèle d'API standard pour HttpClient.

Dans ce cadre, en tant qu'exception interne, "timeouts" devrait probablement être une TimeoutException. En fait, si nous devions combiner cela avec les autres problèmes (concernant la modification de HttpRequestion pour inclure une nouvelle propriété enum similaire à WebExceptionStatus), les développeurs n'auraient qu'à vérifier cette énumération et ne devraient pas du tout inspecter les exceptions internes.

Il s'agit essentiellement de l'option 1) mais en continuant à préserver le modèle d'API compatible avec les applications actuel, les exceptions de niveau supérieur des API HttpClient sont toujours HttpRequestException.

Remarque : tous les "timeouts" résultant de la lecture directe d'un flux de réponse HTTP à partir d'API Stream, tels que :

```c#
var client = nouveau HttpClient();
Flux responseStream = attendre client.GetStreamAsync(url);

int bytesRead = attendre responseStream.ReadAsync(buffer, 0, buffer.Length);
```

devrait continuer à lancer System.IO.IOException etc. en tant qu'exceptions de niveau supérieur (nous avons en fait quelques bogues dans SocketsHttpHandler à ce sujet).

@davidsh suggérez -vous de lancer HttpRequestException même en cas d'annulation explicite de l'utilisateur ? Ceux jettent TaskCanceledException aujourd'hui et ça semble bien. Je ne sais pas si c'est quelque chose que nous voulons changer...
Je serais d'accord pour exposer les délais d'attente côté client comme HttpRequestException au lieu de TaskCanceledException .

@davidsh suggérez -vous de lever HttpRequestException même en cas d'annulation explicite de l'utilisateur ? Celles-ci lancent TaskCanceledException aujourd'hui et cela semble bien. Je ne sais pas si c'est quelque chose que nous voulons changer...

Je dois tester les différents comportements des piles, y compris .NET Framework, pour vérifier les comportements actuels exacts avant de pouvoir répondre à cette question.

De plus, dans certains cas, certaines de nos piles lèvent HttpRequestException avec l'exception interne de OperationCanceledException. TaskCanceledException est un type dérivé de OperationCanceledException.

en cas d'annulation explicite de l'utilisateur

Nous avons également besoin d'une définition claire de ce qu'est "explicite". Par exemple, la définition de la propriété HttpClient.Timeout est-elle "explicite" ? Je pense que nous disons non. Qu'en est-il de l'appel de .Cancel() sur l'objet CancellationTokenSource (qui affecte l'objet passé dans CancellationToken) ? C'est "explicite" ? Il y a probablement d'autres exemples auxquels nous pouvons penser.

.NET Framework lève également TaskCanceledException lorsque vous définissez HttpClient.Timeout .

Si vous ne pouvez pas faire la distinction entre l'annulation du client et l'annulation de la mise en œuvre, cochez simplement les deux. Avant d'utiliser le jeton client, créez-en un dérivé :
```c#
var clientToken = ....
var innerTokenSource = CancellationTokenSource.CreateLinkedTokenSource(clientToken);

If at some point you catch **OperationCancelledException**:
```c#
try
{
     //invocation with innerToken
}
catch(OperationCancelledException oce)
{
      if(clientToken.IsCancellationRequested)
          throw; //as is, it is client initiated and in higher priority
      if(innerToken.IsCancellationRequested)
           //wrap it in HttpException, TimeoutException, whatever, wrap it in another linked token until you process all cases.
}

Fondamentalement, vous filtrez toutes les couches possibles du jeton client à la plus profonde, jusqu'à ce que vous trouviez le jeton qui a provoqué l'annulation.

Des mises à jour sur ce making 2.2 ou 3.0 ? @karelz @davidsh , cc @NickCraver . J'aimerais en quelque sorte qu'il fasse simplement l'option 1) de lancer une TimeoutException, ou alternativement une nouvelle HttpTimeoutException dérivée de HttpRequestException. Merci!

Des mises à jour sur ce making 2.2 ou 3.0 ?

Trop tard pour la 2.2, elle est sortie la semaine dernière ;)

Trop tard pour la 2.2, elle est sortie la semaine dernière ;)

Argh, c'est ce qui se passe quand je fais une pause ;)

Bien qu'un peu loufoque, la solution de contournement la plus simpliste que j'ai trouvée consiste à imbriquer un gestionnaire de capture pour OperationCanceledException, puis à évaluer la valeur de IsCancellationRequested pour déterminer si le jeton d'annulation était le résultat d'un délai d'attente ou d'un avortement de session réel. Il est clair que la logique effectuant l'appel d'API serait probablement dans une logique métier ou une couche de service, mais j'ai consolidé l'exemple pour plus de simplicité :

````csharp
[HttpGet]
Tâche asynchrone publiqueIndex(CancellationToken cancelToken)
{
essayer
{
// Cela se ferait normalement dans une logique métier ou une couche de service plutôt que dans le contrôleur lui-même, mais j'illustre le point ici par souci de simplicité
essayer
{
//Utilisation de HttpClientFactory pour les instances HttpClient
var httpClient = _httpClientFactory.CreateClient();

        //Set an absurd timeout to illustrate the point
        httpClient.Timeout = new TimeSpan(0, 0, 0, 0, 1);

        //Perform call that requires special timeout logic                
        var httpResponse = await httpClient.GetAsync("https://someurl.com/api/long/running");

        //... (if GetAsync doesn't fail, handle the response as desired)
    }
    catch (OperationCanceledException innerOperationCanceled)
    {
        //If a canceled token exception occurs due to a timeout, "IsCancellationRequested" should be false
        if (cancellationToken.IsCancellationRequested)
        {
            //Bubble exception to global handler
            throw innerOperationCanceled;
        }
        else
        {
            //... (perform timeout logic here)
        }                    
    }                

}
catch (OperationCanceledException operationCanceledEx)
{
    _logger.LogWarning(operationCanceledEx, "Request was aborted by the end user.");
    return new StatusCodeResult(499);
}
catch (Exception ex)
{
    _logger.LogError(ex, "An unexepcted error occured.");
    return new StatusCodeResult(500);
}

}
````

J'ai vu d'autres articles sur la façon de gérer ce scénario avec plus d'élégance, mais ils nécessitent tous de creuser dans le pipeline, etc. Libération.

Travailler avec le client http est tout à fait catastrophique. HttpClient doit être supprimé et recommencé à zéro.

Bien qu'un peu loufoque, la solution de contournement la plus simpliste que j'ai trouvée consiste à imbriquer un gestionnaire de capture pour OperationCanceledException, puis à évaluer la valeur de IsCancellationRequested pour déterminer si le jeton d'annulation était le résultat d'un délai d'attente ou d'un avortement de session réel. Il est clair que la logique effectuant l'appel d'API serait probablement dans une logique métier ou une couche de service, mais j'ai consolidé l'exemple pour plus de simplicité :

[HttpGet]
public async Task<IActionResult> Index(CancellationToken cancellationToken)
{
  try
  {
      //This would normally be done in a business logic or service layer rather than in the controller itself but I'm illustrating the point here for simplicity sake
      try
      {
          //Using HttpClientFactory for HttpClient instances      
          var httpClient = _httpClientFactory.CreateClient();

          //Set an absurd timeout to illustrate the point
          httpClient.Timeout = new TimeSpan(0, 0, 0, 0, 1);

          //Perform call that requires special timeout logic                
          var httpResponse = await httpClient.GetAsync("https://someurl.com/api/long/running");

          //... (if GetAsync doesn't fail, handle the response as desired)
      }
      catch (OperationCanceledException innerOperationCanceled)
      {
          //If a canceled token exception occurs due to a timeout, "IsCancellationRequested" should be false
          if (cancellationToken.IsCancellationRequested)
          {
              //Bubble exception to global handler
              throw innerOperationCanceled;
          }
          else
          {
              //... (perform timeout logic here)
          }                    
      }                

  }
  catch (OperationCanceledException operationCanceledEx)
  {
      _logger.LogWarning(operationCanceledEx, "Request was aborted by the end user.");
      return new StatusCodeResult(499);
  }
  catch (Exception ex)
  {
      _logger.LogError(ex, "An unexepcted error occured.");
      return new StatusCodeResult(500);
  }
}

J'ai vu d'autres articles sur la façon de gérer ce scénario avec plus d'élégance, mais ils nécessitent tous de creuser dans le pipeline, etc. Libération.

C'est absolument fou que nous ayons besoin d'écrire ce code atroce. HttpClient est l'abstraction la plus fuyante jamais créée par Microsoft

Je conviens qu'il semble plutôt myope de ne pas inclure d'informations d'exception plus spécifiques concernant les délais d'attente. Cela semble être une chose assez basique pour que les gens veuillent gérer les délais d'attente réels différemment des exceptions de jeton annulées.

Travailler avec le client http est tout à fait catastrophique. HttpClient doit être supprimé et recommencé à zéro.\

Ce n'est pas un commentaire très utile ou constructif @dotnetchris . Les clients et les employés de MSFT sont tous ici pour essayer d'améliorer les choses, et c'est pourquoi des gens comme @karelz sont toujours là pour écouter les commentaires des clients et essayer de s'améliorer. Il n'y a aucune obligation pour eux de faire du développement au grand jour, et ne brûlons pas leur bonne volonté avec de la négativité ou ils pourraient décider de prendre leur balle et de rentrer chez eux, et ce serait pire pour la communauté. Merci! :)

Pourquoi ne pas simplement créer un HttpClient2 (nom temporaire) qui a le comportement attendu et informer, dans la documentation, que "HttpClient est obsolète, veuillez utiliser le HttpClient2" et expliquer la différence entre les 2.

De cette façon, aucune modification avec rupture n'est insérée dans le .NetFramework.

HttpClient ne devrait pas être basé sur la levée d'exceptions. Il n'est pas exceptionnel qu'un hébergeur internet soit indisponible. C'est en fait attendu, étant donné les permutations infinies d'adresses, le cas exceptionnel lui donne quelque chose qui fonctionne.

J'ai même blogué sur la mauvaise utilisation de HttpClient il y a plus de 3 ans. https://dotnetchris.wordpress.com/2015/12/11/working-with-httpclient-is-absurd/

C'est une mauvaise conception flagrante.

Quelqu'un dans Microsoft a-t-il même utilisé HttpClient ? Il n'en a certainement pas l'air.

Ensuite, au-delà de cette horrible surface d'api, il y a toutes sortes d'abstractions qui fuient concernant sa construction et son threading. Je ne peux pas croire qu'il soit possible d'être récupérable et que seul le neuf est une solution viable. HttpClient2, HttpClientSlim est bien ou autre.

L'accès HTTP ne devrait jamais lever d'exceptions. Il doit toujours renvoyer un résultat, le résultat doit pouvoir être inspecté pour l'achèvement, le délai d'attente, la réponse http. La seule raison pour laquelle il devrait être autorisé à lever est que si vous invoquez EnsureSuccess() , il est bon de lever n'importe quelle exception dans le monde. Mais il ne devrait y avoir aucune gestion de flux de contrôle pour un comportement tout à fait normal d'Internet : hôte activé, hôte non opérationnel, délai d'attente de l'hôte. Aucun de ceux-ci n'est exceptionnel.

HttpClient ne devrait pas être basé sur la levée d'exceptions. Il n'est pas exceptionnel qu'un hébergeur internet soit indisponible. C'est en fait attendu, étant donné les permutations infinies d'adresses, le cas exceptionnel lui donne quelque chose qui fonctionne.

@dotnetchris puisqu'il semble que ce sujet vous passionne beaucoup, pourquoi ne pas prendre le temps de rédiger votre proposition d'API complète pour HttpClient2 et de la soumettre à l'équipe pour discussion en tant que nouveau problème ?

Je n'ai pas pris la peine de plonger dans les détails de ce qui rend HttpClient bon ou mauvais d'un niveau technique plus profond, donc je n'ai rien à ajouter aux commentaires de @dotnetchris. Je ne pense certainement pas qu'il soit approprié de critiquer l'équipe de développement de MS, mais je comprends également à quel point il est frustrant de passer des heures et des heures à rechercher des solutions à des situations qui devraient apparemment être très simples à gérer. C'est la frustration de lutter pour comprendre pourquoi certaines décisions/changements ont été faits, en espérant que quelqu'un chez MS répondra à la préoccupation tout en essayant de faire votre travail et de le livrer entre-temps.

Dans mon cas, j'ai un service externe que j'appelle pendant le processus OAuth pour établir des rôles d'utilisateur et si cet appel expire dans un laps de temps spécifié (ce qui arrive malheureusement plus que je ne le souhaiterais), je dois revenir à une méthode secondaire .

Vraiment, il y a une myriade d'autres raisons pour lesquelles je peux voir pourquoi quelqu'un voudrait pouvoir faire la distinction entre un délai d'attente et une annulation. Pourquoi un meilleur support pour un scénario commun comme celui-ci ne semble pas exister, c'est ce que j'ai du mal à comprendre et j'espère vraiment que c'est quelque chose que l'équipe MS réfléchira davantage.

@karelz , que faudrait-il pour que cela soit prévu pour la version 3.0, une proposition d'API de quelqu'un ?

Il ne s'agit pas de proposition d'API. Il s'agit de trouver la bonne implémentation. Il fait partie de notre carnet de commandes 3.0 et est assez élevé sur la liste (derrière HTTP/2 et les scénarios d'entreprise). C'est une très bonne chance d'être traité en 3.0 par notre équipe.
Bien sûr, si quelqu'un de la communauté est motivé, nous prendrons également une contribution.

@karelz comment la communauté peut-elle aider dans ce cas ? La raison pour laquelle j'ai mentionné si une proposition d'API était nécessaire est que je n'étais pas sûr d'après votre message qui commence "Nous avons plusieurs options que faire en cas de dépassement de délai :" si l'équipe avait décidé de son alternative préférée parmi celles que vous avez énumérées. Si vous avez décidé d'une direction, nous pouvons partir de là. Merci!

Techniquement parlant, ce n'est pas une question de proposition d'API. La décision ne nécessitera pas l'approbation de l'API. Cependant, je suis d'accord que nous devrions être alignés sur la façon d'exposer la différence - il y aura probablement des discussions.
Je ne m'attends pas à ce que les options diffèrent trop les unes des autres dans la mise en œuvre - si la communauté veut aider, obtenir la mise en œuvre de l'une des options nous y amènera à 98% (en supposant que l'exception est levée d'un endroit et peut être facilement modifié ultérieurement pour s'adapter aux options [1]-[3] répertoriées ci-dessus).

Salut! Nous avons également rencontré ce problème récemment. Juste curieux de savoir si c'est toujours sur votre radar pour la 3.0 ?

Oui, il est toujours sur notre liste 3.0 - je lui donnerais toujours 90% de chances qu'il soit traité dans 3.0.

Je suis tombé dessus aujourd'hui. J'ai défini un délai d'attente sur HttpClient et cela me donne ceci

{System.Threading.Tasks.TaskCanceledException: A task was canceled.}
CancellationRequested = true

Je suis d'accord avec les autres, c'est déroutant car il ne mentionne pas que le délai d'attente est le problème.

Umm, ok, je suis un peu confus. Tout le monde dans ce fil semble dire que les délais d'attente HTTP lancent un TaskCanceledException mais ce n'est pas ce que j'obtiens... J'obtiens un OperationCanceledException dans .NET Core 2.1...

Si vous voulez lancer une OperationCanceledException pour les délais d'attente, alors très bien, je peux contourner cela, mais j'ai juste passé des heures à essayer de retracer ce qui se passait parce que ce n'est pas documenté correctement:

https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.sendasync

Il indique clairement qu'un HttpRequestException est lancé pour les délais d'attente...

TaskCanceledException dérive de OperationCanceledException.

@stephentoub Oui, mais je ne reçois pas un TaskCanceledException , je reçois un OperationCanceledException .

Comme dans, un bloc catch qui attrape TaskCanceledException n'attrape pas l'exception, il a spécifiquement besoin d'un bloc catch pour un OperationCanceledException .

Je teste maintenant spécifiquement les délais d'attente HTTP en filtrant sur le type d'exception, et la façon dont je sais que c'est un délai d'attente est si ce n'est PAS un TaskCanceledException :

```c#
essayer {
en utilisant (var réponse = attendre s_httpClient.SendAsync(request, _updateCancellationSource.Token).ConfigureAwait(false))
renvoie (int)response.StatusCode < 400 ;
}
catch (OperationCanceledException ex) quand (ex.GetType() != typeof(TaskCanceledException)) {
// Délai d'attente HTTP
retourner faux ;
}
attraper (HttpRequestException) {
retourner faux ;
}

or alternatively this works as well, which might be better:

```c#
            try {
                using (var response = await s_httpClient.SendAsync(request, _updateCancellationSource.Token).ConfigureAwait(false))
                    return (int)response.StatusCode < 400;
            }
            catch (OperationCanceledException ex) when (ex.GetType() == typeof(OperationCanceledException)) {
                // HTTP timeout
                return false;
            }
            catch (HttpRequestException) {
                return false;
            }

Umm, ok, je suis un peu confus. Tout le monde dans ce fil semble dire que les délais d'attente HTTP lancent un TaskCanceledException mais ce n'est pas ce que j'obtiens... J'obtiens un OperationCanceledException dans .NET Core 2.1...

Si vous voulez lancer une OperationCanceledException pour les délais d'attente, alors très bien, je peux contourner cela, mais j'ai juste passé des heures à essayer de retracer ce qui se passait parce que ce n'est pas documenté correctement:

https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.sendasync

Il indique clairement qu'un HttpRequestException est lancé pour les délais d'attente...

Avez-vous lu tout le fil parce qu'il y a en fait plusieurs réponses où OperationCanceledExecption est référencé, y compris l'exemple de code que j'ai posé qui montre un moyen (pas aussi propre que je le voudrais mais c'est un moyen) de gérer explicitement les délais d'attente.

@Hobray Oui, j'ai - qu'est-ce qui vous fait penser que je ne l'ai pas fait ? Cela n'explique toujours pas combien de personnes dans ce fil sont censées recevoir TaskCanceledException sur les délais d'attente, mais ce n'est pas le cas.

J'ai également vu votre exemple de code, mais malheureusement, il contient une condition de concurrence que je ne crois pas que vous ayez prise en compte. Le code posté que j'utilise évite ce problème et ne nécessite pas d'accès au jeton d'annulation, ce qui peut être important si vous essayez d'attraper le délai d'attente plus en amont.

@mikernet Peut-être que le libellé m'a découragé. Vous avez raison de dire que je n'avais pas envisagé une condition de concurrence possible. Je me rends compte que ce n'est pas une solution géniale, mais si vous souhaitez expliquer ce que j'ai négligé, j'aimerais l'entendre.

Une chose que j'ai remarquée est que l'exception exacte que vous obtenez semble varier selon que le délai d'attente ou l'annulation se produit ou non dans un appel Controller vs middleware. À l'intérieur d'une invocation de middleware, il semble que ce soit simplement TaskCanceledException . Malheureusement, je n'ai pas le temps de fouiller dans les bibliothèques de code .NET Core pour comprendre pourquoi, mais c'est quelque chose que j'ai remarqué.

@Hobray Haha ouais bien sûr - désolé, je ne voulais pas être énigmatique à propos de la condition de course, j'étais juste sur mon téléphone pendant que j'étais en train de taper ce dernier message, ce qui rendait difficile à expliquer.

La condition de concurrence dans votre solution se produit si une annulation de tâche est initiée entre le délai d'expiration HttpClient et le moment où vous vérifiez l'état d'annulation du jeton d'annulation. Dans ma situation, j'ai besoin d'une exception d'annulation de tâche pour continuer à bouillonner afin que le code en amont puisse détecter l'annulation de l'utilisateur. Je dois donc pouvoir attraper l'exception uniquement si elle est réellement dépassée - c'est pourquoi j'utilise when pour filtrer les captures. L'utilisation de votre solution de vérification du jeton peut entraîner un OperationCanceledException non géré passant le filtre et bouillonnant et plantant le programme, car quelqu'un annulant l'opération en amont s'attendra à ce qu'un TaskCanceledException soit lancé, pas un OperationCanceledException .

Si le code appelant la méthode HttpClient est censé gérer à la fois le délai d'attente et l'annulation de la tâche, la distinction entre le délai d'attente et l'annulation de l'utilisateur lorsque la condition de concurrence se produit peut ne pas être importante pour vous, selon la situation... mais si vous écrivez du code au niveau de la bibliothèque et que vous devez laisser l'annulation de la tâche se développer à un niveau supérieur, cela fait définitivement une différence.

Si ce que vous dites est vrai et que les délais d'attente génèrent différents types d'exceptions en fonction du contexte d'appel, cela me semble définitivement être un problème qui doit être résolu. Il n'y a aucun moyen que ce soit ou devrait être le comportement prévu.

Intéressant sur la condition de course possible. Je n'avais pas pensé à ça. Heureusement, pour mon cas d'utilisation, ce ne serait pas vraiment un problème.

En ce qui concerne les variations d'exception, j'ai besoin de trouver un peu de temps pour réduire le middleware sur lequel j'ai travaillé pour voir s'il est cohérent avec ce que j'ai vu plusieurs fois lors du débogage. Les stupides appels de préchargement de l'omnibox Chrome ont été là où je l'ai vu dans mon middleware lorsque le moment était venu. Je pense que je peux créer un exemple simple pour le prouver définitivement d'une manière ou d'une autre.

Je ne sais toujours pas quelle devrait être la solution de contournement correcte.

Ma solution est la plus correcte dans les situations où les délais d'attente http lancent OperationCanceledException. Je ne peux pas reproduire la situation où TaskCanceledException est levée et personne n'a fourni un moyen de reproduire ce comportement, donc je dis testez votre environnement et s'il correspond au mien, c'est la bonne solution.

Si, dans certains contextes d'exécution, une TaskCanceledException est levée, ma solution échoue en tant que solution à usage général pour ces contextes et l'autre solution proposée est la "meilleure" solution mais contient une condition de concurrence qui peut ou non être importante pour votre situation, c'est pour à vous d'évaluer.

J'aimerais entendre plus de détails de la part de quelqu'un qui prétend qu'il reçoit des exceptions TaskCanceledExceptions, car ce serait clairement un énorme bogue. Je suis un peu sceptique que cela se produise réellement.

@mikernet que TaskCanceledException ou OperationCanceledException soit lancé est en grande partie hors de propos. C'est un détail de mise en œuvre sur lequel vous ne devriez pas vous fier. Ce que vous pouvez faire est d'attraper OperationException (qui attrapera également TaskCanceledException ) et de vérifier si le jeton d'annulation que vous avez passé est annulé, comme décrit dans le commentaire de Hobray (https://github.com/ dotnet/corefx/issues/20296#issuecomment-449510983).

@thomaslevesque J'ai déjà expliqué pourquoi cette approche est un problème. Sans aborder cela, votre commentaire n'est pas très utile - il s'agit simplement de ressasser des déclarations avant que je ne rejoigne la conversion sans résoudre le problème que j'ai décrit.

Quant à "c'est un détail d'implémentation" - oui, mais malheureusement je dois m'y fier car sinon il n'y a pas de bon moyen de déterminer si la tâche a été réellement annulée ou expirée. Il devrait y avoir à 100% un moyen de déterminer à partir de l'exception lequel de ces deux scénarios s'est produit afin d'éviter la condition de concurrence décrite. OperationCanceledException est juste un mauvais choix à cause de cela... il y a déjà un TimeoutException dans le cadre qui fait un meilleur candidat.

Les exceptions pour les tâches asynchrones ne doivent pas reposer sur la vérification des données dans un autre objet qui peut également être modifié de manière asynchrone pour déterminer la cause de l'exception si un cas d'utilisation courant consiste à prendre une décision de branchement en fonction de la cause de l'exception. C'est la mendicité pour les problèmes de condition de course.

De plus, en ce qui concerne le type d'exception étant un détail d'implémentation ... je ne pense pas vraiment que ce soit vrai pour les exceptions, du moins pas en termes de pratiques de documentation .NET bien établies. Nommez un autre exemple dans le .NET Framework où la documentation indique "lève l'exception X lorsque Y se produit" et le cadre lève en fait une sous-classe accessible au public de cette exception, sans parler d'une sous-classe accessible au public qui est utilisée pour un cas d'exception complètement différent sur la même méthode ! Cela semble être une mauvaise forme et je n'ai jamais rencontré cela auparavant au cours de mes 15 années de programmation .NET.

Quoi qu'il en soit, TaskCanceledException devrait être réservé à l'annulation de tâche initiée par l'utilisateur, pas aux délais d'attente - comme je l'ai dit, nous avons déjà une exception pour cela. Les bibliothèques doivent capturer toutes les annulations de tâches internes et lancer des exceptions plus appropriées. Les gestionnaires d'exceptions pour les annulations de tâches qui ont été lancées avec des jetons d'annulation de tâche ne doivent pas être responsables de la gestion d'autres problèmes non liés qui pourraient survenir pendant l'opération asynchrone.

@mikernet désolé, j'ai raté cette partie de la discussion. Oui, il y a une condition de concurrence à laquelle je n'avais pas pensé. Mais ce n'est peut-être pas un problème aussi grave qu'il n'y paraît... La condition de concurrence se produit si le jeton d'annulation est signalé après l'expiration du délai mais avant de vérifier l'état du jeton, n'est-ce pas ? Dans ce cas, peu importe si un délai d'attente s'est produit si l'opération est annulée de toute façon. Donc, vous laisserez un OperationCanceledException bouillonner, laissant croire à l'appelant que l'opération a été annulée avec succès, ce qui est un mensonge puisqu'un délai d'attente s'est produit, mais cela n'a pas d'importance pour l'appelant, puisqu'il voulait interrompez quand même l'opération.

Il devrait y avoir à 100% un moyen de déterminer à partir de l'exception lequel de ces deux scénarios s'est produit afin d'éviter la condition de concurrence décrite. OperationCanceledException est juste un mauvais choix à cause de cela... il y a déjà un TimeoutException dans le cadre qui fait un meilleur candidat.

Je suis entièrement d'accord avec cela, le comportement actuel est clairement mauvais. À mon avis, la meilleure option pour résoudre ce problème est l'option 1 du commentaire de @karelz . Oui, c'est un changement radical, mais je soupçonne que la plupart des bases de code ne gèrent pas correctement ce scénario de toute façon, donc cela n'aggravera pas les choses.

@thomaslevesque Eh bien oui, mais j'ai en quelque sorte abordé cela. Cela peut ou non être important pour certaines applications. Chez moi c'est pourtant le cas. L'utilisateur de ma méthode de bibliothèque n'attend pas OperationCanceledException lorsqu'il annule une tâche - il attend TaskCanceledException et il devrait pouvoir l'attraper et être "en sécurité". Si je vérifie le jeton d'annulation, vois qu'il a été annulé et transmet l'exception, je viens de transmettre une exception qui ne sera pas interceptée par leur gestionnaire et fera planter le programme. Ma méthode n'est pas censée lancer des délais d'attente.

Bien sûr, il existe des moyens de contourner cela ... Je peux simplement lancer un nouveau TaskCanceledException enveloppé, mais je perds la trace de la pile de niveau supérieur, et les délais d'attente HTTP sont traités de manière très différente des annulations de tâches dans mon service - les délais d'attente HTTP vont dans un cache pendant une longue période, contrairement aux annulations de tâches. Je ne veux pas manquer la mise en cache des délais d'attente réels en raison de la condition de concurrence.

Je suppose que je pourrais attraper OperationCanceledException et vérifier le jeton d'annulation - s'il a été annulé, vérifiez également le type d'exception pour vous assurer qu'il s'agit d'un TaskCanceledException avant de relancer pour éviter que tout le programme ne plante. Si l'une de ces conditions est fausse, il doit s'agir d'un délai d'attente. Le seul problème qui reste dans ce cas est la condition de concurrence qui est subtile, mais lorsque vous avez des milliers de requêtes HTTP par minute sur un service fortement chargé avec des délais d'expiration serrés, cela se produit.

Quoi qu'il en soit, c'est hacky et, comme vous l'avez indiqué, cela devrait être résolu d'une manière ou d'une autre.

(La condition de concurrence ne se produira que dans les environnements où TaskCanceledException est lancé au lieu de OperationCanceledException , où que ce soit, mais au moins la solution est à l'abri de générer un type d'exception inattendu et ne s'appuyer sur les détails de mise en œuvre afin de continuer à fonctionner raisonnablement bien si les détails de mise en œuvre changent).

L'exception de temporisation lancée par HttpClient en cas de temporisation http ne doit pas être de type System.Net.HttpRequestException (ou un descendant ?) au lieu de System.TimeoutException générique ? Par exemple, le bon vieux WebClient lance WebException avec une propriété Status qui peut être définie sur WebExceptionStatus.Timeout .

Malheureusement, nous ne pourrons pas terminer cela pour la version 3.0. Passer à l'avenir.
C'est toujours au sommet de notre carnet de commandes, mais de manière réaliste, nous ne pourrons pas le faire pour 3.0 :(.

@karelz oh non :( J'allais juste republier en disant comment j'ai rencontré cela à nouveau dans la deuxième entreprise consécutive. Combien de temps la fenêtre serait-elle pour soumettre un PR pour 3.0 si je voulais m'y attaquer moi-même ?

J'ai du mal à suivre exactement ce qui se passe dans le groupe de commentaires ci-dessus avec des personnes signalant des différences entre l'obtention de TaskCancelledException et OperationCancelledException en fonction du contexte, quelqu'un de MSFT peut-il éclairer cela ?

Aimez ce commentaire de @mikernet :

Comme dans, un bloc catch qui attrape TaskCanceledException n'attrape pas l'exception, il a spécifiquement besoin d'un bloc catch pour une OperationCanceledException.

cc @davidsh @stephentoub peut-être ?

TaskCanceledException dérive de OperationCanceledException et contient quelques informations supplémentaires. Attrapez simplement OperationCanceledException.

@karelz est -ce que l'option 1 que vous avez publiée à l'origine, en la faisant simplement lancer une exception Timeout, serait complètement désagréable?

Des choses comme StackExchange.Redis lancent une exception dérivée de l'exception Timeout, c'est pourquoi il serait bien de simplement intercepter l'exception Timeout pour HttpClient...

Je comprends @stephentoub compte tenu de l'héritage, juste ce commentaire ci-dessous de @mikernet me souffle l'esprit... Ce comportement qu'il décrit devrait être une sorte de bug ?? Mais si c'est vrai, cela influencerait le correctif ou la solution de contournement…

"un bloc catch qui attrape TaskCanceledException n'attrape pas l'exception, il a spécifiquement besoin d'un bloc catch pour une OperationCanceledException"

Ou dites-vous que certains endroits lancent un OperationEx et d'autres un TaskEx? Comme peut-être que les gens voient quelque chose provenant du middleware ASP et pensent que cela vient de HttpClient ?

juste ce commentaire ci-dessous de @mikernet est époustouflant... Ce comportement qu'il décrit devrait être une sorte de bogue ??

Pourquoi est-ce que ça "t'épate" ? Si l'exception B dérive de l'exception A, un bloc catch pour B n'attrapera pas une exception qui est concrètement A, par exemple si j'ai un bloc catch pour ArgumentNullException, il n'attrapera pas " throw new ArgumentException(...)".

Ou dites-vous que certains endroits lancent un OperationEx et d'autres un TaskEx?

À droite. En général, le code ne doit supposer qu'OCE ; la façon dont les choses sont écrites, certains codes peuvent finir par lancer le TCE dérivé, par exemple une tâche terminée via TaskCompletionSource.TrySetCanceled produira une TaskCanceledException.

Merci Stephen, d'après les sons des choses dans le fil, plusieurs personnes avaient l'impression que TaskCE était lancé partout, ce qui aurait rendu le comportement mentionné par mikernet insensé... Maintenant que vous avez clarifié, cela fait son observation logique :)

@stephentoub , ces conseils sont-ils documentés quelque part ? Ping @guardrex

En général, le code ne devrait supposer que OCE ...

Je travaille sur l'ensemble de documents ASP.NET ces jours-ci. Ouvrir un problème pour les chats dotnet/docs ...

https://github.com/dotnet/docs/issues

Est-il possible de relancer cette conversation là où il y avait des propositions concrètes avec @karelz et @davidsh ? C'est comme si quelqu'un qui possédait le modèle d'API HttpClient devait prendre des décisions sur la direction à suivre. Il y a eu des discussions sur les exceptions de haut niveau étant toujours HttpRequestException (autres que des annulations explicites ??), mélangées à un sujet semblant à plus long terme sur un refactor plus important pour modifier HttpRequestion pour inclure une nouvelle propriété enum similaire à WebExceptionStatus...

On a juste l'impression que tout ce problème est en train de se compliquer par rapport au changement de code réel nécessaire. Y a-t-il d'autres personnes qui ont une participation/propriété dans le modèle d'API HttpClient qui devraient être mises en boucle afin de prendre des décisions ? Merci beaucoup à tous !

J'ai le même problème ici -- cependant, cancellationToken.IsCancellationRequested renvoie true :

    public class TimeoutHandler : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            try
            {
                var response = await base.SendAsync(request, cancellationToken);
                response.EnsureSuccessStatusCode();

                return response;
            }
            catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
            {
                throw new TimeoutException("Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding.");
            }
        }

Je force un timeout. L'exception déclenchée est TaskCancelledException avec IOException comme exception interne :

System.Threading.Tasks.TaskCanceledException: The operation was canceled. ---> System.IO.IOException: Unable to read data from the transport connection: A operação de E/S foi anulada devido a uma saída de thread ou a uma requisição de aplicativo. ---> System.Net.Sockets.SocketException: A operação de E/S foi anulada devido a uma saída de thread ou a uma requisição de aplicativo
   --- End of inner exception stack trace ---
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error)
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.GetResult(Int16 token)
   at System.Net.Http.HttpConnection.FillAsync()
   at System.Net.Http.HttpConnection.ReadNextResponseHeaderLineAsync(Boolean foldedHeadersAllowed)
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithNtConnectionAuthAsync(HttpConnection connection, HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

...

(Désolé pour le texte d'exception traduit - apparemment, quelqu'un a pensé que ce serait une bonne idée de l'apporter également à .NET Core)

Comme vous pouvez le voir, j'espérais vérifier IsCancellationRequest pour contourner ce problème comme d'autres l'ont suggéré, mais même sur les délais d'attente, il retourne true .

Actuellement, je pense à implémenter une minuterie pour vérifier si le délai d'attente configuré est passé. Mais, évidemment, je veux mourir , c'est une solution de contournement horrible.

Salut @FelipeTaveira ,

Votre approche ne fonctionne pas, car l'annulation est gérée en amont de votre TimeoutHandler , par le HttpClient lui-même. Votre gestionnaire reçoit un jeton d'annulation qui est annulé par le HttpClient lorsqu'un Timeout se produit. Donc, vérifier que le jeton d'annulation n'est pas annulé ne fonctionne que dans le code qui appelle HttpClient , pas dans le code appelé par HttpClient .

La façon de le faire fonctionner avec un gestionnaire personnalisé est de désactiver le HttpClient Timeout en le définissant sur Timeout.InfiniteTimeSpan et en contrôlant le comportement du délai d'attente dans votre propre gestionnaire, comme montré dans cet article (code complet ici )

@thomaslevesque Ah, merci ! J'avais lu votre code et pensé que cette partie concernait la prise en charge de la configuration du délai d'attente par demande et je n'avais pas remarqué qu'il était également nécessaire de prendre en charge la chose TimeoutException .

J'espère que cela sera résolu à l'avenir.

Pour ce que ça vaut, je trouve aussi que TaskCanceledException jeté sur les délais d'attente est déroutant.

Je peux attester que TaskCanceledExceptions étaient une source majeure de confusion lors de la gestion des problèmes de production dans mon emploi précédent. Nous encapsulons généralement les appels à HttpClient et lançons des TimeoutExceptions le cas échéant.

@eiriktsarpalis merci. Nous prévoyons de l'aborder en 5.0, afin que vous puissiez le récupérer dans ce laps de temps si vous le souhaitez, maintenant que vous avez rejoint notre équipe 😇.

1 année? 😵 Où est cette agilité promise de .NET Core ?

Juger l'agilité de l'ensemble du projet sur la base d'un seul problème est un peu ... injuste.
Oui, c'est un peu gênant, nous avons toujours ce problème, cependant, il est difficile à résoudre (avec un impact de compatibilité) et il y avait juste des objectifs commerciaux plus élevés à aborder en premier (en réseau) - voir les changements de jalons à travers l'histoire de ce problème.

BTW : Comme dans de nombreux autres projets OSS, il n'y a pas de SLA ou d'ETA ou de promesses sur le moment où le problème sera résolu. Tout dépend des priorités, des autres travaux, de la difficulté et du nombre d'experts pouvant s'attaquer au problème.

Juger l'agilité de l'ensemble du projet sur la base d'un seul problème est un peu ... injuste.

Eh bien, c'était une question honnête. Ça fait déjà deux ans et quand j'ai vu que ça allait en reprendre un j'ai été un peu surpris. Aucune offense ne signifiait cependant 🙂. Je sais que pour résoudre ce problème, il y avait / il y a quelques questions auxquelles il fallait d'abord répondre.

Dans un monde de compromis... Je n'aime pas ça mais voici une autre option à mâcher :

Nous pourrions continuer à lancer TaskCanceledException , mais définir son CancellationToken sur une valeur sentinelle.

c# catch(TaskCanceledException ex) when(ex.CancellationToken == HttpClient.TimeoutToken) { }

Et un autre : lancer un nouveau HttpTimeoutException qui dérive de TaskCanceledException .

Et un autre : lancer un nouveau HttpTimeoutException qui dérive de TaskCanceledException .

Je pense que cela répondrait à toutes les préoccupations. Il est facile d'attraper spécifiquement un délai d'attente, et c'est toujours un TaskCanceledException , donc le code qui attrape actuellement cette exception attrapera également la nouvelle (pas de changement de rupture).

Et un autre : lancer une nouvelle HttpTimeoutException qui dérive de TaskCanceledException.

Cela semble plutôt moche, mais probablement un bon compromis.

Et un autre : lancer un nouveau HttpTimeoutException qui dérive de TaskCanceledException .

Je pense que cela répondrait à toutes les préoccupations. Il est facile d'attraper spécifiquement un délai d'attente, et c'est toujours un TaskCanceledException , donc le code qui attrape actuellement cette exception attrapera également la nouvelle (pas de changement de rupture).

Eh bien, il _pourrait_ y avoir des gens qui écrivent du code comme celui-ci :

try
{
  // ...
}
catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException))
{
}

Ce serait donc techniquement un changement radical.

Je pense aussi que c'est un peu le pire des deux mondes. Nous n'obtiendrons toujours pas un TimeoutException général en même temps avec un changement de rupture.

il pourrait y avoir des gens qui écrivent du code comme celui-ci, donc ce serait techniquement un changement radical

FWIW, à peu près tous les changements sont susceptibles de se casser, donc pour progresser sur quoi que ce soit, il faut tracer une ligne quelque part. Dans corefx, nous ne considérons pas le retour ou le lancement d'un type plus dérivé comme une rupture.
https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/breaking-change-rules.md#exceptions

Dans un monde de compromis... Je n'aime pas ça mais voici une autre option à mâcher :

Nous pourrions continuer à lancer TaskCanceledException , mais définir son CancellationToken sur une valeur sentinelle.

catch(TaskCanceledException ex) when(ex.CancellationToken == HttpClient.TimeoutToken)
{
}

J'aime vraiment celle-ci. Y a-t-il un autre inconvénient à cela en plus d'être "" moche "" ? Qu'est-ce que ex.CancellationToken aujourd'hui lorsqu'un délai d'attente se produit ?

Y a-t-il un autre inconvénient à cela en plus d'être "" moche ""

Il serait vraiment facile pour quelqu'un de penser que TimeoutToken peut être transmis et utilisé pour signaler l'annulation lorsqu'un délai d'attente se produit, car c'est la conception qui est utilisée ailleurs pour de telles choses.

J'aime vraiment celle-ci. Y a-t-il un autre inconvénient à cela en plus d'être "" moche "" ? Qu'est-ce que ex.CancellationToken aujourd'hui lorsqu'un délai d'attente se produit ?

C'est beaucoup moins intuitif que d'attraper un type d'exception spécifique, qui peut être explicitement mentionné dans la documentation.

J'ai l'habitude de considérer un OperationCanceledException et des types dérivés pour signifier une exécution interrompue, pas une erreur d'application. L'exception HttpClient émise lors de l'expiration du délai est généralement une erreur - il y a eu un appel à une API HTTP et celle-ci n'était pas disponible. Ce comportement entraîne soit des complications lors du traitement des exceptions à un niveau supérieur, par exemple, dans le middleware de pipeline ASP.NET Core, car ce code doit savoir qu'il existe deux versions de OperationCanceledException , erreur et non-erreur, ou il vous oblige à envelopper chaque appel HttpClient dans un bloc lancer-attraper.

Et un autre : lancer une nouvelle HttpTimeoutException qui dérive de TaskCanceledException.

Cela semble plutôt moche, mais probablement un bon compromis.

Est-ce vraiment si moche, étant donné les contraintes de conception impliquées et les considérations de rétrocompatibilité ? Je ne sais pas si quelqu'un dans ce fil a proposé une solution manifestement supérieure qui n'a aucun inconvénient (que ce soit l'utilisation ou la pureté de la conception, etc.)

Nous voulons examiner cela plus en profondeur que simplement changer le type d'exception. Il y a aussi un problème de longue date de "où avons-nous expiré ?" -- par exemple, il est possible qu'une demande expire sans jamais avoir touché le fil -- que nous devons examiner, et une solution doit être compatible avec la direction dans laquelle nous allons.

Nous voulons examiner cela plus en profondeur que simplement changer le type d'exception. Il y a aussi un problème de longue date de "où avons-nous expiré ?" -- par exemple, il est possible qu'une demande expire sans jamais avoir touché le fil -- que nous devons examiner, et une solution doit être compatible avec la direction dans laquelle nous allons.

Est-ce vraiment quelque chose que vous devez savoir ? Je veux dire, gérerez-vous l'erreur différemment selon l'endroit où elle a expiré ? Pour moi, le simple fait de savoir qu'un délai d'attente s'est produit est généralement suffisant.
Cela ne me dérange pas si vous voulez en faire plus, je ne veux tout simplement pas que ce changement rate le train .NET 5 en raison d'exigences supplémentaires :wink:

Je pense que le problème soulevé par @scalablecory est remarquable, mais je conviens que cela ne devrait pas empêcher que cela soit résolu plus tôt. Vous pouvez décider d'un mécanisme pour permettre l'évaluation de la raison du délai d'attente, mais ne tardez pas à résoudre le problème d'exception pour cela... vous pouvez toujours ajouter une propriété Reason à l'exception plus tard.

Je suis d'accord avec @thomaslevesque; Je n'ai jamais entendu cela comme un gros problème auparavant. D'où cela vient-il ?

Est-ce un cas de débogage assez spécialisé où il serait acceptable de creuser la raison d'exiger une petite action explicite, alors que pour un développeur général, il n'aurait qu'à savoir/se soucier de gérer une seule chose.

Nous voulons examiner cela plus en profondeur que simplement changer le type d'exception. Il y a aussi un problème de longue date de "où avons-nous expiré ?" -- par exemple, il est possible qu'une demande expire sans jamais avoir touché le fil -- que nous devons examiner, et une solution doit être compatible avec la direction dans laquelle nous allons.

Je suppose que l'implication ici est qu'il devrait être de la responsabilité de chaque HttpMessageHandler de lancer le bon type d'exception de délai d'attente plutôt que HttpClient de réparer les choses.

Je suppose que l'implication ici est qu'il devrait être de la responsabilité de chaque HttpMessageHandler de lancer le bon type d'exception de délai d'attente plutôt que HttpClient de réparer les choses.

Je ne vois pas comment c'est possible avec l'abstraction définie aujourd'hui. HttpMessageHandler n'a aucune connaissance du délai d'attente de HttpClient ; en ce qui concerne un HttpMessageHandler, il vient de remettre un jeton d'annulation, qui pourrait avoir une annulation demandée pour un certain nombre de raisons, y compris le jeton d'annulation de l'appelant étant signalé, les CancelPendingRequests du HttpClient étant appelés, le délai d'attente imposé du HttpClient expirant, etc.

Est-ce vraiment quelque chose que vous devez savoir ? Je veux dire, gérerez-vous l'erreur différemment selon l'endroit où elle a expiré ? Pour moi, le simple fait de savoir qu'un délai d'attente s'est produit est généralement suffisant.

Il est utile de savoir si le serveur a peut-être traité votre demande. C'est également idéal pour les diagnostics - un serveur distant n'expire pas mais le client signale des délais d'attente est une expérience frustrante.

@davidsh peut avoir plus d'informations.

Je suis d'accord avec @thomaslevesque ; Je n'ai jamais entendu cela comme un gros problème auparavant. D'où cela vient-il ?

C'était un point soulevé lors d'une récente réunion du NCL. Il se peut que nous décidions que ce n'est pas assez important pour exposer ou laisser retarder les choses, mais cela vaut d'abord une discussion rapide.

@dotnet/ncl @stephentoub Mettons celui-ci au lit. Nous savons que tout ce que nous ferons se brisera subtilement. Cela semble être la moins mauvaise option. Je propose d'avancer avec :

jeter un nouveau HttpTimeoutException qui dérive de TaskCanceledException .

Des objections ?

lancer une nouvelle HttpTimeoutException qui dérive de TaskCanceledException.

Comment fait-on cela en pratique ? Cela signifie probablement que nous devons nettoyer notre code et trouver tous les endroits qui appellent CancellationToken.ThrowIfCancellationRequested et le convertir à la place :

c# if (token.IsCancellationRequested) throw new HttpTimeoutException(EXTRASTUFF, token);

ainsi que des endroits explicites que nous pourrions également appeler OperationCancelledException .

Existe-t-il une option qui ne nécessite pas de passer en revue tout ce code connexe de toute façon?

Existe-t-il une option qui ne nécessite pas de passer en revue tout ce code connexe de toute façon?

Probablement pas, mais il pourrait y avoir des endroits que nous ne contrôlons pas (pas encore vraiment sûrs) et nous devrons donc potentiellement récupérer une TaskCancelledException/OperationCancelledException aléatoire et relancer la nouvelle HttpTimeoutException.

Cela signifie probablement que nous devons nettoyer notre code et trouver tous les endroits qui appellent CancellationToken.ThrowIfCancellationRequested et le convertir à la place :

Existe-t-il une option qui ne nécessite pas de passer en revue tout ce code connexe de toute façon?

Pas exactement.

En ce qui concerne les gestionnaires, tout ce qu'ils reçoivent est un CancellationToken : ils ne savent pas et ne se soucient pas d'où vient ce jeton, qui l'a produit ou pourquoi il pourrait avoir une demande d'annulation. Tout ce qu'ils savent, c'est qu'une annulation peut être demandée à un moment donné, et ils lèvent une exception d'annulation en réponse. Donc, il n'y a rien à faire ici dans les gestionnaires.

C'est HttpClient lui-même qui crée le CancellationTokenSource pertinent et lui définit un délai d'expiration :
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L488
Le délai d'expiration est entièrement l'invention de HttpClient lui-même, et lui seul sait s'il souhaite traiter une exception d'annulation qui est apparue comme étant spéciale.

Cela signifie que le code référencé ci-dessus devrait être refactorisé pour suivre le délai d'expiration séparément, soit en tant que CancellationTokenSource distinct, soit plus probablement en gérant la logique de délai d'expiration par lui-même, et en plus d'annuler le CTS, en définissant également un indicateur qu'il peut puis vérifiez également. Cela ne sera probablement pas payant, dans la mesure où nous ajouterons des allocations supplémentaires ici, mais du point de vue du code, c'est relativement simple.

Puis à des endroits dans HttpClient comme :
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L536 -L541
et
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L562 -L566
lorsqu'il attrape une exception, il doit effectuer une annulation de cas particulier, pour vérifier si le délai d'attente a expiré et, le cas échéant, supposer que c'est la cause de l'annulation et lancer une nouvelle exception du type souhaité.

il aurait besoin d'une annulation de cas particulier, pour vérifier si le délai d'attente a expiré, et si c'était le cas, supposer que c'était la cause de l'annulation et lancer une nouvelle exception du type souhaité.

Comment cela fonctionne-t-il si la propriété HttpClient.Timeout n'est pas impliquée ? Peut-être est-il réglé sur "infini". Qu'en est-il des jetons d'annulation transmis directement aux API HttpClient (GetAsync, SendAync, etc.) ? Je suppose que ça marcherait pareil ?

Comment cela fonctionne-t-il si la propriété HttpClient.Timeout n'est pas impliquée ?

Je ne comprends pas la question... c'est le seul temps mort dont nous parlons ici. S'il est défini sur Infini, l'annulation ne sera jamais causée par ce délai d'attente et il n'y aura rien à faire. Nous avons déjà un cas particulier Timeout == Infinite et continuerions à :
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L481

Qu'en est-il des jetons d'annulation transmis directement aux API HttpClient (GetAsync, SendAync, etc.) ? Je suppose que ça marcherait pareil ?

Nous combinons tout jeton transmis avec l'un de nos propres créations :
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L485
Nous suivrions notre propre gestion de HttpClient.Timeout, comme mentionné dans mes commentaires précédents. Si l'appelant choisit de transmettre un jeton qu'il a configuré avec un délai d'attente, le traitement spécial lui appartient : en ce qui nous concerne, il s'agit simplement d'une demande d'annulation.

Des objections ?

Pourquoi introduire un nouveau type d'exception dérivé qui "concurrence" l'exception TimeoutException existante plutôt que, par exemple, de lancer une exception d'annulation qui encapsule une TimeoutException en tant qu'exception interne ? Si l'objectif est de permettre à un consommateur de déterminer par programmation si l'annulation est due à un délai d'expiration, cela suffira-t-il ? Il semblerait communiquer les mêmes informations, sans avoir besoin d'une nouvelle surface.

Pourquoi introduire un nouveau type d'exception dérivé qui "concurrence" l'exception TimeoutException existante plutôt que, par exemple, de lancer une exception d'annulation qui encapsule une TimeoutException en tant qu'exception interne

Par commodité, surtout. C'est plus facile et plus intuitif à écrire

catch(HttpTimeoutException)

que

catch(OperationCanceledException ex) when (ex.InnerException is TimeoutException)

Cela étant dit, dans les deux cas, cela me convient, tant qu'il est raisonnablement facile d'identifier un délai d'attente.

dire, lancer une exception d'annulation qui encapsule une TimeoutException comme exception interne ?

Je pense que c'est un bon compromis. Cela aiderait à lever l'ambiguïté des journaux d'exceptions, ce qui était ma principale source de désagrément face à ce type de délais d'attente. Il communique probablement aussi mieux le fait qu'il s'agit du HttpClient qui annule une demande, plutôt qu'un délai d'attente déclenché par le gestionnaire sous-jacent. En outre, aucun changement de rupture.

Triage:

  • Nous mettrons à jour ce scénario pour avoir un InnerException de TimeoutException avec un message de journalisation/diagnostic qui distingue facilement l'annulation comme étant causée par le déclenchement du délai d'attente.
  • Nous mettrons à jour la documentation avec des conseils sur les façons d'inspecter par programme pour différencier l'annulation : vérifier TimeoutException , utiliser un Timeout infini et passer un CancellationToken avec un délai d'expiration, etc.

Nous reconnaissons que toute solution ici implique un certain compromis, et pensons que cela fournira un bon niveau de compatibilité avec le code existant.

Pourquoi introduire un nouveau type d'exception dérivé qui "concurrence" l'exception TimeoutException existante plutôt que, par exemple, de lancer une exception d'annulation qui encapsule une TimeoutException en tant qu'exception interne

Par commodité, surtout. C'est plus facile et plus intuitif à écrire

catch(HttpTimeoutException)

que

catch(OperationCanceledException ex) when (ex.InnerException is TimeoutException)

Cela étant dit, dans les deux cas, cela me convient, tant qu'il est raisonnablement facile d'identifier un délai d'attente.

@scalablecory Je suis déçu que j'ai semblé manquer la discussion de quelques heures - mon expérience de communication de ce type de choses avec les développeurs LoB .NET généraux est qu'il est beaucoup plus facile pour les gens de comprendre/adopter que d'avoir à attraper une HttpTimeoutException dérivée n'oubliez pas d'utiliser un filtre d'exception sur une exception interne. Tout le monde ne sait même pas ce qu'est un filtre d'exception.

En même temps, je reconnais totalement que c'est l'un de ces scénarios où il n'y a pas de solution gagnante/sans compromis claire, comme vous l'avez dit.

Nous étions d'accord sur ce point @ericsampson mais ce serait un changement radical pour de nombreux utilisateurs existants. Comme @scalablecory l'a mentionné, nous nous sommes compromis pour apporter des améliorations tout en limitant les dommages de compatibilité.

Quand je pense aux exceptions, en général, je considère que l'approche d'emballage est meilleure. Dériver l'exception de OperationCanceledException rend cette exception disponible uniquement en utilisation asynchrone. Et même il est serré aux tâches. Comme nous l'avons vu dans le passé, il y a eu plusieurs évolutions des approches asynchrones et je considère la tâche comme un détail d'implémentation, mais TimeoutException, par exemple, fait partie du concept de socket qui ne changera pas.

Nous étions d'accord sur ce point @ericsampson mais ce serait un changement radical pour de nombreux utilisateurs existants. Comme @scalablecory l'a mentionné, nous nous sommes compromis pour apporter des améliorations tout en limitant les dommages de compatibilité.

Bien que je ne sois pas nécessairement en désaccord avec l'option d'emballage, je pense qu'il convient de souligner que - pour autant que j'ai compris - le problème n'est pas que l'option dérivée est un changement avec rupture, mais que l'option d'emballage est plus simple. Comme @stephentoub l'a mentionné plus tôt dans la discussion, la levée d'une exception dérivée n'est pas considérée comme une modification avec rupture par les directives corefx.

Dériver HttpTimeoutException de TaskCanceledException viole « est une » relation entre ces deux exceptions. Il fera toujours penser au code d'appel que l'annulation s'est produite, à moins qu'il ne gère spécifiquement HttpTimeoutException. Il s'agit essentiellement de la même manipulation que nous devons faire au moment où nous vérifions si le jeton d'annulation est annulé pour déterminer s'il s'agit d'un délai d'attente ou non. En outre, avoir deux exceptions de délai d'attente dans la bibliothèque principale est source de confusion.
L'ajout de TimeoutException en tant qu'exception interne à TaskCanceledException ne fournira aucune information supplémentaire dans la plupart des cas, car nous pouvons déjà vérifier le jeton d'annulation et déterminer s'il a expiré ou non.

Je pense que la seule solution sensée est de changer l'exception lancée sur le délai d'attente en TimeoutException. Oui, ce sera un changement de rupture, mais le problème couvre déjà plusieurs versions majeures et les changements de rupture sont à quoi servent les versions majeures.

La documentation des versions existantes peut-elle être mise à jour pour indiquer que les méthodes peuvent lancer TaskCanceledException et/ou OperationCanceledException (les deux types concrets semblent être une possibilité) lorsqu'un délai d'attente se produit ? Actuellement, la documentation indique que HttpRequestException traite des délais d'attente, ce qui est incorrect et trompeur.

@qidydl c'est le plan ... documentera entièrement toutes les façons dont les délais d'attente peuvent se manifester et comment les détailler.

Merci @alnikola et @scalablecory et @davidsh et @karelz et tous les autres impliqués dans cette discussion/mise en œuvre, j'apprécie vraiment.

Cette page vous a été utile?
0 / 5 - 0 notes