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

119

Cuando un usuario carga una página, realiza una o más solicitudes ajax, que llegan a los controladores ASP.NET Web API 2. Si el usuario navega a otra página, antes de que se completen estas solicitudes ajax, el navegador cancela las solicitudes. Nuestro ELMAH HttpModule luego 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.<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()

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

Mirando el stacktrace, veo que la excepción se lanza 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 asíncronos.
  • No importa dónde agregue el registro de errores, no puedo detectar la excepción en el código de usuario
Bates Westmoreland
fuente
1
También hemos visto las mismas excepciones (TaskCanceledException y OperationCanceledException) con la versión actual de las bibliotecas Katana.
David McClelland
Encontré algunos detalles más sobre cuándo ocurren ambas excepciones y descubrí que esta solución solo funciona contra una de ellas. Aquí hay algunos detalles: stackoverflow.com/questions/22157596/…
Ilya Chernomordik

Respuestas:

78

Este es un error en ASP.NET Web API 2 y, desafortunadamente, 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, puede 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 existe una pequeña posibilidad de que suceda, porque el cliente podría desconectarse justo después de que el controlador de mensajes verifique el token de cancelación, pero antes de que el código de API web de nivel superior realice la misma verificación. Pero creo que ayudará en la mayoría de los casos.

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
fuente
2
Como actualización, esto captura algunas de las solicitudes. Todavía vemos bastantes en nuestros registros. Gracias por la solución. Esperando una solución.
Bates Westmoreland
2
@KiranChalla - Puedo confirmar que la actualización a 5.2.2 todavía tiene estos errores.
KnightFox
2
Aún recibo el error. He utilizado la sugerencia anterior, cualquier otra pista.
M2012
3
Cuando probé la recomendación anterior, todavía recibí Excepciones cuando la solicitud se canceló incluso antes de que se pasara a SendAsync(puede simular esto manteniendo presionada F5en el navegador una URL que realiza solicitudes a su Api. Resolví este problema también agregando el if (cancellationToken.IsCancellationRequested)cheque encima de la llamada a SendAsync. Ahora las excepciones ya no aparecen cuando el navegador cancela rápidamente las solicitudes.
seangwright
2
Encontré algunos detalles más sobre cuándo ocurren ambas excepciones y descubrí que esta solución solo funciona contra una de ellas. Aquí hay algunos detalles: stackoverflow.com/a/51514604/1671558
Ilya Chernomordik
17

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

HttpConfiguration.Services.Add(typeof(IExceptionLogger), myWebApiExceptionLogger); 
Sombrío Zeineddine
fuente
Parece que el problema con este enfoque es que el error aún aparece en el manejo de errores de Global.asax ... Evento aunque no se envía al controlador de excepciones
Ilya Chernomordik
14

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

#if !DEBUG
app.Use(async (ctx, next) =>
{
    try
    {
        await next();
    }
    catch (OperationCanceledException)
    {
    }
});
#endif
huysentruitw
fuente
2
Recibí este error principalmente del contexto OWIN y este lo dirige mejor
NitinSingh
3

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

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

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

Sin embargo, parece que esta excepción 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 manejarla con su código.

En este caso, usted debería ser capaz de atraparlo como primera excepción casualidad, con AppDomain.CurrentDomain.FirstChanceException, aquí es cómo . Entiendo que esto puede no ser lo que estás buscando.

ratio nasal
fuente
1
Ninguno de los dos me permitió manejar la excepción tampoco.
Bates Westmoreland
@BatesWestmoreland, ¿ni siquiera FirstChanceException? ¿Ha intentado manejarlo con una clase estática que persiste en las solicitudes HTTP?
noseratio
2
El problema que estoy tratando de resolver es detectarlo e ignorar estas excepciones. Usar AppDomain.UnhandledExceptiono AppDomain.CurrentDomain.FirstChanceExceptionpuede permitirme inspeccionar la excepción, pero no detectarla e ignorarla. No vi una forma de marcar estas excepciones como manejadas usando ninguno de estos enfoques. Corrígeme si estoy equivocado.
Bates Westmoreland
2

A veces obtengo las mismas 2 excepciones en mi aplicación Web API 2, sin embargo, puedo detectarlas con el Application_Errormétodo Global.asax.csy usando un filtro de excepción genérico .

Lo curioso es, sin embargo, que 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 lo hacen o al menos no deberían fallar eso, pero puedo estar equivocado). Sospecho que estos errores aparecen debido a la expiración del tiempo de espera o la cancelación explícita del cliente, pero habría esperado que se trataran dentro del marco ASP.NET y no se propagaran fuera de él como excepciones no controladas.

Gabriel S.
fuente
En mi caso, estas excepciones ocurren porque el navegador cancela la solicitud cuando el usuario navega a una nueva URL.
Bates Westmoreland
1
Veo. En mi caso, las solicitudes se emiten a través de la WinHTTPAPI, no desde un navegador.
Gabriel S.
2

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

  1. OperationCanceledException
  2. TaskCanceledException

El primero ocurre si la conexión se interrumpe mientras se ejecuta su código en el controlador (o posiblemente también algún código del sistema relacionado con eso). Mientras que el segundo ocurre si la conexión se cae mientras la ejecución está dentro de un atributo (p. Ej.AuthorizeAttribute ).

Por lo tanto, la solución alternativa proporcionada ayuda a mitigar parcialmente la primera excepción, no hace nada para ayudar con la segunda. En el último caso, el TaskCanceledExceptionocurre durantebase.SendAsync propia llamada en lugar de que el token de cancelación se establezca en verdadero.

Puedo ver dos formas de resolver estos:

  1. Simplemente ignorando ambas excepciones en global.asax. Luego surge la pregunta de si es posible ignorar repentinamente algo importante.
  2. Haciendo un try / catch adicional en el controlador (aunque no es a prueba de balas + todavía existe la posibilidad de que lo TaskCanceledExceptionque ignoremos sea uno que queramos registrar.

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 única forma en que descubrí que podemos intentar identificar las excepciones incorrectas es verificando si stacktrace contiene algo de Asp.Net. Aunque no parece muy robusto.

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");
}
Ilya Chernomordik
fuente
1
En su código sugerido, crea la responsevariable dentro del intento y la devuelve fuera del intento. Eso no puede funcionar, ¿verdad? Además, ¿dónde usa la IsAspNetBugException?
Schoof
1
No, eso no puede funcionar, solo tiene que declararse fuera del bloque try / catch, por supuesto, e inicializarse con algo como una tarea completada. Es solo un ejemplo de la solución que de todos modos no es a prueba de balas. En cuanto a la otra pregunta, la usa en el controlador Global.Asax OnError. Si no usa ese para registrar sus mensajes, no necesita preocuparse de todos modos. Si lo hace, este es un ejemplo de cómo filtra los "no errores" del sistema.
Ilya Chernomordik
1

Hemos estado recibiendo la misma excepción, intentamos usar la solución alternativa de @ dmatson, pero aún así obtendríamos alguna excepción. Nos ocupamos de ello 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 terminado, la conexión aún persistía durante dos minutos después de la conexión original.

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

yo añadí response.Headers.ConnectionClose = true; a la SendAsync MessageHandler y de lo que puedo decir a los clientes se están cerrando las conexiones y no están experimentando el problema más.

Sé que esta no es la mejor solución, pero funciona en nuestro caso. También estoy bastante seguro de que, en cuanto al rendimiento, esto no es algo que le gustaría hacer si su API recibe varias llamadas del mismo cliente seguidas.

Michael Margala
fuente