web-dev-qa-db-fra.com

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

Lorsqu'un utilisateur charge une page, il fait une ou plusieurs demandes ajax, qui atteignent les contrôleurs ASP.NET Web API 2. Si l'utilisateur accède à une autre page, avant que ces demandes ajax ne soient terminées, elles sont annulées par le navigateur. Notre module HttpModule ELMAH enregistre ensuite deux erreurs pour chaque demande 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 la pile, 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 # L41

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

Il semble être en dehors du code d'utilisateur ...

Remarques:

  • J'utilise ASP.NET Web API 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 consignation des erreurs, je ne parviens pas à détecter l'exception dans le code utilisateur
112

Il s'agit d'un bogue dans ASP.NET Web API 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 réparer 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 de l'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 est déclenché. Si la réponse n'a pas de contenu, le bogue ne devrait pas être déclenché. Il est encore possible que cela se produise, car le client peut se déconnecter dès que le gestionnaire de messages vérifie le jeton d'annulation, mais avant que le code de l'API Web de niveau supérieur effectue le même contrôle. 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;
    }
}
76
dmatson

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

HttpConfiguration.Services.Add(typeof(IExceptionLogger), myWebApiExceptionLogger); 
15
Shaddy Zeineddine

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

#if !DEBUG
app.Use(async (ctx, next) =>
{
    try
    {
        await next();
    }
    catch (OperationCanceledException)
    {
    }
});
#endif
10
huysentruitw

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

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

Ensuite, ayez une classe static (avec un constructeur static) dans votre application Web, ce qui permettrait de gérer AppDomain.UnhandledException.

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

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

3
noseratio

J'ai parfois les mêmes 2 exceptions dans mon application Web API 2, mais je peux les attraper avec le Application_Error méthode dans Global.asax.cs et en utilisant un générique filtre d'exception .

Ce qui est amusant, c’est que je préfère ne pas intercepter ces exceptions, car j’ai toujours consigné toutes les exceptions non gérées qui peuvent bloquer l’application (ces 2, toutefois, ne sont pas pertinentes pour moi et ne semblent pas ou du moins ne devraient pas s’arrêter). mais je me trompe peut-être). Je soupçonne que ces erreurs apparaissent en raison d'une expiration de délai d'expiration ou d'une annulation explicite du client, mais je m'attendais à ce qu'elles soient traitées dans le cadre ASP.NET et non propagées à l'extérieur de celui-ci en tant qu'exceptions non gérées.

2
Gabriel S.

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

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

La plupart des erreurs concernaient toutes "Timer_ConnectionIdle". Je cherchai autour de moi et il me sembla que même si l'appel de l'API Web était terminé, la connexion persistait encore deux minutes après la connexion initiale.

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; à SendAsync MessageHandler et, d’après ce que je peux dire, les clients ferment les connexions et nous n’éprouvons plus le problème.

Je sais que ce n'est pas la meilleure solution, mais cela fonctionne dans notre cas. Je suis également certain que, du point de vue des performances, ce n'est pas ce que vous voudriez faire si votre API reçoit plusieurs appels consécutifs du même client.

1
Michael Margala

J'ai trouvé un peu plus de détails sur cette erreur. Il existe 2 exceptions possibles pouvant survenir:

  1. OperationCanceledException
  2. TaskCanceledException

Le premier survient 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 survient si la connexion est interrompue alors que l'exécution est à l'intérieur d'un attribut (par exemple, AuthorizeAttribute).

Ainsi, le solution de contournement fourni aide à atténuer partiellement la première exception, il ne fait rien pour aider la seconde. Dans ce dernier cas, le TaskCanceledException se produit pendant le base.SendAsync appelle lui-même plutôt que ce jeton d'annulation étant défini sur true.

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

  1. Tout en ignorant les deux exceptions dans global.asax. Vient ensuite la question de savoir s'il est possible d'ignorer soudainement quelque chose d'important.
  2. Faire un try/catch supplémentaire dans le gestionnaire (bien que ce ne soit pas pare-balles +, il est toujours possible que TaskCanceledException que 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 pour moi de trouver des exceptions erronées consiste à vérifier si stacktrace contient des éléments Asp.Net. Ne semble pas très robuste cependant.

P.S. 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");
}
1
Ilya Chernomordik