ASP.NET Web API OperationCanceledException cuando el navegador cancela la solicitud

Cuando un usuario carga una página, realiza una o más solicitudes ajax, que afectan a los controladores ASP.NET Web API 2. Si el usuario navega a otra página, antes de completar estas solicitudes ajax, el navegador cancela las solicitudes. Nuestro ELMAH HttpModule registra dos errores por cada solicitud cancelada:

Error 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.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.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.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.d__0.MoveNext() 

Error 2:

 System.OperationCanceledException: The operation was canceled. at System.Threading.CancellationToken.ThrowIfCancellationRequested() at System.Web.Http.WebHost.HttpControllerHandler.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.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.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) 

Al observar el stacktrace, veo que la excepción se está lanzando desde aquí: https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Http.WebHost/HttpControllerHandler.cs# L413

Mi pregunta es: ¿cómo puedo manejar e ignorar estas excepciones?

Parece estar fuera del código de usuario …

Notas:

  • Estoy usando ASP.NET Web API 2
  • Los puntos finales de la API web son una combinación de métodos asíncronos y no asincrónicos.
  • No importa dónde agregue el registro de errores, no puedo detectar la excepción en el código de usuario
    • Global.asax Applicaiton_Error
    • TaskScheduler.UnobservedTaskException
    • Error de filtrado de void ErrorLog_Filtering ( https://code.google.com/p/elmah/wiki/ErrorFiltering )

Este es un error en ASP.NET Web API 2 y, lamentablemente, no creo que haya una solución que siempre tenga éxito. Archivamos un error para arreglarlo de nuestro lado.

En última instancia, el problema es que devolvemos una tarea cancelada a ASP.NET en este caso, y ASP.NET trata una tarea cancelada como una excepción no controlada (registra el problema en el registro de eventos de la aplicación).

Mientras tanto, puedes probar algo como el siguiente código. Agrega un controlador de mensajes de nivel superior que elimina el contenido cuando se activa el token de cancelación. Si la respuesta no tiene contenido, el error no debería activarse. Todavía hay una pequeña posibilidad de que ocurra, porque el cliente podría desconectarse justo después de que el manejador de mensajes verifique el token de cancelación pero antes de que el código API de nivel superior haga el mismo chequeo. Pero creo que ayudará en la mayoría de los casos.

David

 config.MessageHandlers.Add(new CancelledTaskBugWorkaroundMessageHandler()); class CancelledTaskBugWorkaroundMessageHandler : DelegatingHandler { protected override async Task 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; } } 

Al implementar un registrador de excepciones para WebApi, se recomienda extender la clase System.Web.Http.ExceptionHandling.ExceptionLogger lugar de crear un ExceptionFilter. Los internos de WebApi no llamarán al método de registro de ExceptionLoggers para las solicitudes canceladas (sin embargo, los filtros de excepción las obtendrán). Esto es por diseño.

 HttpConfiguration.Services.Add(typeof(IExceptionLogger), myWebApiExceptionLogger); 

Aquí hay otra solución para este problema. Simplemente agregue un middleware OWIN personalizado al comienzo de la canalización de OWIN que capte la OperationCanceledException :

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

Puede intentar cambiar el comportamiento de manejo de excepciones de tarea TPL predeterminado a través de web.config :

      

Luego, tenga una clase static (con un constructor static ) en su aplicación web, que manejaría AppDomain.UnhandledException .

Sin embargo, parece que esta excepción en realidad se está manejando en algún lugar dentro del tiempo de ejecución de ASP.NET Web API , incluso antes de que tenga la oportunidad de manejarlo con su código.

En este caso, debería ser capaz de detectarlo como una excepción de la primera oportunidad, con AppDomain.CurrentDomain.FirstChanceException , así es cómo . Entiendo que esto puede no ser lo que estás buscando.

A veces obtengo las mismas 2 excepciones en mi aplicación Web API 2, sin embargo, puedo verlas con el método Application_Error en Global.asax.cs y usar un filtro de excepción genérico.

Lo curioso es, sin embargo, prefiero no detectar estas excepciones, porque siempre registro todas las excepciones no controladas que pueden bloquear la aplicación (estas 2, sin embargo, son irrelevantes para mí y aparentemente no o al menos no deberían bloquearse). eso, pero puedo estar equivocado). Sospecho que estos errores aparecen debido a la caducidad del tiempo de espera o la cancelación explícita del cliente, pero esperaba que se trataran dentro del marco ASP.NET y no se propagaran fuera de él como excepciones no controladas.

He encontrado un poco más detalles sobre este error. Hay 2 posibles excepciones que pueden suceder:

  1. OperationCanceledException
  2. TaskCanceledException

El primero ocurre si se corta la conexión mientras se ejecuta el código en el controlador (o posiblemente algún código del sistema alrededor de eso también). Mientras que el segundo ocurre si la conexión se cae mientras la ejecución está dentro de un atributo (por ejemplo, AuthorizeAttribute ).

Entonces, la solución provista ayuda a mitigar parcialmente la primera excepción, no ayuda en nada con la segunda. En el último caso, la TaskCanceledException produce durante la llamada a base.SendAsync en lugar de que el token de cancelación se establezca en verdadero.

Puedo ver dos formas de resolver esto:

  1. Ignorando ambas excepciones en global.asax. Luego viene la pregunta si es posible ignorar repentinamente algo importante en su lugar.
  2. Hacer un try / catch adicional en el controlador (aunque no es a prueba de balas + aún existe la posibilidad de que TaskCanceledException que ignoramos sea uno que deseemos registrar.

 config.MessageHandlers.Add(new CancelledTaskBugWorkaroundMessageHandler()); class CancelledTaskBugWorkaroundMessageHandler : DelegatingHandler { protected override async Task 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 única forma en que descubrí que podemos tratar de identificar las excepciones incorrectas es comprobando si stacktrace contiene algo de Asp.Net. No parece muy robusto sin embargo.

PD Así es como filtro estos errores:

 private static bool IsAspNetBugException(Exception exception) { return (exception is TaskCanceledException || exception is OperationCanceledException) && exception.StackTrace.Contains("System.Web.HttpApplication.ExecuteStep"); } 

Hemos estado recibiendo la misma excepción, hemos intentado utilizar la solución de @ dmatson, pero todavía obtendríamos alguna excepción. Lo tratamos hasta hace poco. Notamos que algunos registros de Windows crecían a un ritmo alarmante.

Archivos de error ubicados en: C: \ Windows \ System32 \ LogFiles \ HTTPERR

La mayoría de los errores fueron todos para “Timer_ConnectionIdle”. Busqué alrededor y parecía que a pesar de que la llamada a la API web había finalizado, la conexión aún persistía durante dos minutos después de la conexión original.

Luego pensé que deberíamos tratar de cerrar la conexión en la respuesta y ver qué pasa.

Agregué response.Headers.ConnectionClose = true; al SendAsync MessageHandler y por lo que puedo decir, los clientes están cerrando las conexiones y ya no estamos experimentando el problema.

Sé que esta no es la mejor solución, pero funciona en nuestro caso. También estoy bastante seguro de que, en lo que respecta al rendimiento, esto no es algo que quieras hacer si tu API recibe múltiples llamadas del mismo cliente una detrás de la otra.