OperationCanceledException de l'API Web ASP.NET lorsque le navigateur annule la demande

119

Lorsqu'un utilisateur charge une page, il effectue une ou plusieurs requêtes ajax, qui atteignent les contrôleurs ASP.NET Web API 2. Si l'utilisateur accède à une autre page, avant que ces requêtes ajax ne soient terminées, les requêtes sont annulées par le navigateur. Notre ELMAH HttpModule enregistre ensuite deux erreurs pour chaque requête annulée:

Erreur 1:

System.Threading.Tasks.TaskCanceledException: A task was canceled.
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at System.Web.Http.Controllers.ApiControllerActionInvoker.<InvokeActionAsyncCore>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.AuthorizationFilterAttribute.<ExecuteAuthorizationFilterAsyncCore>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at System.Web.Http.Controllers.ExceptionFilterResult.<ExecuteAsync>d__0.MoveNext()

Erreur 2:

System.OperationCanceledException: The operation was canceled.
   at System.Threading.CancellationToken.ThrowIfCancellationRequested()
   at System.Web.Http.WebHost.HttpControllerHandler.<WriteBufferedResponseContentAsync>d__1b.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.WebHost.HttpControllerHandler.<CopyResponseAsync>d__7.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.WebHost.HttpControllerHandler.<ProcessRequestAsyncCore>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.TaskAsyncHelper.EndTask(IAsyncResult ar)
   at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

En regardant le stacktrace, je vois que l'exception est levée à partir d'ici: https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Http.WebHost/HttpControllerHandler.cs# L413

Ma question est la suivante: comment puis-je gérer et ignorer ces exceptions?

Cela semble être en dehors du code utilisateur ...

Remarques:

  • J'utilise l'API Web ASP.NET 2
  • Les points de terminaison de l'API Web sont un mélange de méthodes asynchrones et non asynchrones.
  • Peu importe où j'ajoute la journalisation des erreurs, je ne parviens pas à détecter l'exception dans le code utilisateur
Bates Westmoreland
la source
1
Nous avons vu les mêmes exceptions (TaskCanceledException et OperationCanceledException) avec la version actuelle des bibliothèques Katana également.
David McClelland
J'ai trouvé plus de détails sur le moment où les deux exceptions se produisent et j'ai compris que cette solution de contournement ne fonctionne que contre l'un d'entre eux. Voici quelques détails: stackoverflow.com/questions/22157596/…
Ilya Chernomordik

Réponses:

78

Il s'agit d'un bogue dans l'API Web ASP.NET 2 et malheureusement, je ne pense pas qu'il existe une solution de contournement qui réussira toujours. Nous avons déposé un bug pour le corriger de notre côté.

En fin de compte, le problème est que nous renvoyons une tâche annulée à ASP.NET dans ce cas, et ASP.NET traite une tâche annulée comme une exception non gérée (il enregistre le problème dans le journal des événements d'application).

En attendant, vous pouvez essayer quelque chose comme le code ci-dessous. Il ajoute un gestionnaire de messages de niveau supérieur qui supprime le contenu lorsque le jeton d'annulation se déclenche. Si la réponse n'a pas de contenu, le bogue ne devrait pas être déclenché. Il y a encore une petite possibilité que cela puisse arriver, car le client pourrait se déconnecter juste après que le gestionnaire de messages ait vérifié le jeton d'annulation, mais avant que le code de l'API Web de niveau supérieur ne fasse la même vérification. Mais je pense que cela aidera dans la plupart des cas.

David

config.MessageHandlers.Add(new CancelledTaskBugWorkaroundMessageHandler());

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

        // Try to suppress response content when the cancellation token has fired; ASP.NET will log to the Application event log if there's content in this case.
        if (cancellationToken.IsCancellationRequested)
        {
            return new HttpResponseMessage(HttpStatusCode.InternalServerError);
        }

        return response;
    }
}
Dmatson
la source
2
En tant que mise à jour, cela capture certaines des demandes. Nous en voyons encore pas mal dans nos logs. Merci pour le contournement. Dans l'attente d'un correctif.
Bates Westmoreland
2
@KiranChalla - Je peux confirmer que la mise à niveau vers la version 5.2.2 contient toujours ces erreurs.
KnightFox
2
J'obtiens toujours l'erreur. J'ai utilisé la suggestion ci-dessus, tout autre indice.
M2012
3
Lorsque j'ai essayé la recommandation ci-dessus, j'ai toujours reçu des exceptions lorsque la demande avait été annulée avant même qu'elle ne soit transmise SendAsync(vous pouvez simuler cela en maintenant enfoncée F5dans le navigateur une URL qui envoie des demandes à votre API. J'ai également résolu ce problème ajouter leif (cancellationToken.IsCancellationRequested) coche au-dessus de l'appel à SendAsync. Désormais, les exceptions n'apparaissent plus lorsque le navigateur annule rapidement les demandes.
seangwright
2
J'ai trouvé plus de détails sur le moment où les deux exceptions se produisent et j'ai compris que cette solution de contournement ne fonctionne que contre l'un d'entre eux. Voici quelques détails: stackoverflow.com/a/51514604/1671558
Ilya Chernomordik
17

Lors de l'implémentation d'un enregistreur d'exceptions pour WebApi, il est recommandé d'étendre la System.Web.Http.ExceptionHandling.ExceptionLoggerclasse plutôt que de créer un ExceptionFilter. Les internes WebApi n'appelleront pas la méthode Log des ExceptionLoggers pour les demandes annulées (cependant, les filtres d'exception les obtiendront). C'est par conception.

HttpConfiguration.Services.Add(typeof(IExceptionLogger), myWebApiExceptionLogger); 
Shaddy Zeineddine
la source
Il semble que le problème avec cette approche est que l'erreur apparaît toujours dans la gestion des erreurs Global.asax ... Événement bien qu'il ne soit pas envoyé au gestionnaire d'exceptions
Ilya Chernomordik
14

Voici une autre solution de contournement pour ce problème. Ajoutez simplement un middleware OWIN personnalisé au début du pipeline OWIN qui intercepte OperationCanceledException:

#if !DEBUG
app.Use(async (ctx, next) =>
{
    try
    {
        await next();
    }
    catch (OperationCanceledException)
    {
    }
});
#endif
huysentruitw
la source
2
J'obtenais cette erreur principalement du contexte OWIN et celui-ci la cible mieux
NitinSingh
3

Vous pouvez essayer de modifier le comportement de gestion des exceptions de tâche TPL par défaut via web.config:

<configuration> 
    <runtime> 
        <ThrowUnobservedTaskExceptions enabled="true"/> 
    </runtime> 
</configuration>

Ensuite, ayez une staticclasse (avec un staticconstructeur) dans votre application Web, qui gérerait AppDomain.UnhandledException.

Cependant, il semble que cette exception soit en fait gérée quelque part dans le runtime de l'API Web ASP.NET , avant même que vous n'ayez une chance de la gérer avec votre code.

Dans ce cas, vous devriez pouvoir l'attraper comme une exception de 1ère chance, avec AppDomain.CurrentDomain.FirstChanceException, voici comment . Je comprends que ce n'est peut-être pas ce que vous recherchez.

noseratio
la source
1
Aucun de ceux-ci ne m'a permis de gérer l'exception non plus.
Bates Westmoreland
@BatesWestmoreland, même pas FirstChanceException? Avez-vous essayé de le gérer avec une classe statique qui persiste dans les requêtes HTTP?
noseratio
2
Le problème que j'essaie de résoudre pour attraper et ignorer ces exceptions. Utiliser AppDomain.UnhandledExceptionou AppDomain.CurrentDomain.FirstChanceExceptionpeut me permettre d'inspecter l'exception, mais pas d'attraper et d'ignorer. Je n'ai pas vu de moyen de marquer ces exceptions comme étant gérées en utilisant l'une ou l'autre de ces approches. Corrigez-moi si je me trompe.
Bates Westmoreland
2

J'obtiens parfois les mêmes 2 exceptions dans mon application Web API 2, mais je peux les attraper avec la Application_Errorméthode Global.asax.cset en utilisant un filtre d'exception générique .

Ce qui est drôle, c'est que je préfère ne pas attraper ces exceptions, car je consigne toujours toutes les exceptions non gérées qui peuvent faire planter l'application (ces 2, cependant, ne sont pas pertinentes pour moi et apparemment ne se bloquent pas ou du moins ne devraient pas planter il, mais je peux me tromper). Je soupçonne que ces erreurs apparaissent en raison de l'expiration du délai d'expiration ou d'une annulation explicite du client, mais je me serais attendu à ce qu'elles soient traitées à l'intérieur du framework ASP.NET et non propagées en dehors de celui-ci comme des exceptions non gérées.

Gabriel S.
la source
Dans mon cas, ces exceptions se produisent parce que le navigateur annule la demande lorsque l'utilisateur navigue vers une nouvelle URL.
Bates Westmoreland
1
Je vois. Dans mon cas, les demandes sont émises via l' WinHTTPAPI, pas à partir d'un navigateur.
Gabriel S.
2

J'ai trouvé un peu plus de détails sur cette erreur. Il y a 2 exceptions possibles qui peuvent se produire:

  1. OperationCanceledException
  2. TaskCanceledException

Le premier se produit si la connexion est interrompue pendant que votre code dans le contrôleur s'exécute (ou éventuellement un code système autour de cela également). Alors que le second se produit si la connexion est abandonnée alors que l'exécution est à l'intérieur d'un attribut (par exemple AuthorizeAttribute).

Ainsi, la solution de contournement fournie permet d'atténuer partiellement la première exception, elle ne fait rien pour aider avec la seconde. Dans ce dernier cas, le TaskCanceledExceptionse produit pendant l' base.SendAsyncappel lui-même plutôt que le jeton d'annulation étant défini sur true.

Je peux voir deux façons de résoudre ces problèmes:

  1. Ignorant simplement les deux exceptions dans global.asax. Vient ensuite la question de savoir s'il est possible d'ignorer soudainement quelque chose d'important à la place?
  2. Faire un try / catch supplémentaire dans le gestionnaire (bien que ce ne soit pas à l'épreuve des balles + il est toujours possible que ce TaskCanceledExceptionque nous ignorons soit celui que nous voulons enregistrer.

config.MessageHandlers.Add(new CancelledTaskBugWorkaroundMessageHandler());

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

            // Try to suppress response content when the cancellation token has fired; ASP.NET will log to the Application event log if there's content in this case.
            if (cancellationToken.IsCancellationRequested)
            {
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }
        }
        catch (TaskCancellationException)
        {
            // Ignore
        }

        return response;
    }
}

La seule façon dont j'ai compris que nous pouvons essayer de localiser les mauvaises exceptions est de vérifier si stacktrace contient des éléments Asp.Net. Cela ne semble pas très robuste cependant.

PS Voici comment je filtre ces erreurs:

private static bool IsAspNetBugException(Exception exception)
{
    return
        (exception is TaskCanceledException || exception is OperationCanceledException) 
        &&
        exception.StackTrace.Contains("System.Web.HttpApplication.ExecuteStep");
}
Ilya Chernomordik
la source
1
Dans votre code suggéré, vous créez la responsevariable à l'intérieur de l'essai et la renvoyez en dehors de l'essai. Cela ne peut pas fonctionner, n'est-ce pas? Où utilisez-vous également l'exception IsAspNetBugException?
Schoof
1
Non, cela ne peut pas fonctionner, il doit simplement être déclaré en dehors du bloc try / catch bien sûr et initialisé avec quelque chose comme la tâche terminée. C'est juste un exemple de solution qui n'est de toute façon pas à l'épreuve des balles. Quant à l'autre question, vous l'utilisez dans le gestionnaire Global.Asax OnError. Si vous ne l'utilisez pas pour enregistrer vos messages, vous n'avez pas à vous inquiéter de toute façon. Si vous le faites, ceci est un exemple de la façon dont vous filtrez les «non-erreurs» du système.
Ilya Chernomordik
1

Nous avons reçu la même exception, nous avons essayé d'utiliser la solution de contournement de @ dmatson, mais nous obtiendrions toujours une exception. Nous l'avons traité jusqu'à récemment. Nous avons remarqué que certains journaux Windows augmentaient à un rythme alarmant.

Fichiers d'erreur situés dans: C: \ Windows \ System32 \ LogFiles \ HTTPERR

La plupart des erreurs concernaient toutes "Timer_ConnectionIdle". J'ai cherché partout et il semblait que même si l'appel de l'API Web était terminé, la connexion persistait encore pendant deux minutes après la connexion d'origine.

J'ai alors pensé que nous devrions essayer de fermer la connexion dans la réponse et voir ce qui se passe.

J'ai ajouté response.Headers.ConnectionClose = true;au SendAsync MessageHandler et d'après ce que je peux dire, les clients ferment les connexions et nous ne rencontrons plus le problème.

Je sais que ce n'est pas la meilleure solution, mais cela fonctionne dans notre cas. Je suis également presque sûr que ce n'est pas quelque chose que vous voudriez faire si votre API reçoit plusieurs appels du même client dos à dos.

Michael Margala
la source