ASP.NET Web API OperationCanceledException quando o navegador cancela a solicitação

119

Quando um usuário carrega uma página, ele faz uma ou mais solicitações ajax, que atingem os controladores ASP.NET Web API 2. Se o usuário navegar para outra página, antes que essas solicitações ajax sejam concluídas, as solicitações são canceladas pelo navegador. Nosso ELMAH HttpModule então registra dois erros para cada solicitação cancelada:

Erro 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()

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

Olhando para o stacktrace, vejo que a exceção está sendo lançada a partir daqui: https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Http.WebHost/HttpControllerHandler.cs# L413

Minha pergunta é: como posso lidar e ignorar essas exceções?

Parece estar fora do código do usuário ...

Notas:

  • Estou usando ASP.NET Web API 2
  • Os endpoints da API da Web são uma mistura de métodos assíncronos e não assíncronos.
  • Não importa onde eu adiciono o registro de erros, não consigo capturar a exceção no código do usuário
Bates Westmoreland
fonte
1
Vimos as mesmas exceções (TaskCanceledException e OperationCanceledException) com a versão atual das bibliotecas Katana.
David McClelland
Eu encontrei mais alguns detalhes sobre quando as duas exceções acontecem e descobri que esta solução alternativa só funciona em uma delas. Aqui estão alguns detalhes: stackoverflow.com/questions/22157596/…
Ilya Chernomordik

Respostas:

78

Este é um bug no ASP.NET Web API 2 e, infelizmente, não acho que haja uma solução alternativa que sempre terá sucesso. Arquivamos um bug para consertá-lo do nosso lado.

Em última análise, o problema é que retornamos uma tarefa cancelada ao ASP.NET neste caso, e o ASP.NET trata a tarefa cancelada como uma exceção não tratada (ele registra o problema no log de eventos do aplicativo).

Enquanto isso, você pode tentar algo como o código abaixo. Ele adiciona um manipulador de mensagens de nível superior que remove o conteúdo quando o token de cancelamento é disparado. Se a resposta não tiver conteúdo, o bug não deve ser acionado. Ainda há uma pequena possibilidade de que isso aconteça, porque o cliente pode se desconectar logo após o manipulador de mensagens verificar o token de cancelamento, mas antes que o código da API da Web de nível superior faça a mesma verificação. Mas acho que vai ajudar na maioria dos 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
fonte
2
Como uma atualização, isso captura algumas das solicitações. Ainda vemos alguns em nossos registros. Obrigado pela solução alternativa. Ansioso por uma correção.
Bates Westmoreland
2
@KiranChalla - Posso confirmar que a atualização para 5.2.2 ainda contém esses erros.
KnightFox
2
Ainda estou recebendo o erro. Usei a sugestão acima, quaisquer outras pistas.
M2012
3
Quando tentei a recomendação acima, ainda recebi Exceções quando a solicitação foi cancelada antes mesmo de ser passada para SendAsync(você pode simular isso mantendo pressionado F5no navegador um url que faz solicitações à sua Api. Resolvi esse problema por também adicionando a if (cancellationToken.IsCancellationRequested)marca de verificação acima da chamada para SendAsync. Agora as exceções não aparecem mais quando o navegador cancela rapidamente as solicitações.
seangwright
2
Eu encontrei mais alguns detalhes sobre quando as duas exceções acontecem e descobri que essa solução alternativa só funciona em uma delas. Aqui estão alguns detalhes: stackoverflow.com/a/51514604/1671558
Ilya Chernomordik
17

Ao implementar um registrador de exceções para WebApi, é recomendável estender a System.Web.Http.ExceptionHandling.ExceptionLoggerclasse em vez de criar um ExceptionFilter. Os internos WebApi não chamarão o método Log de ExceptionLoggers para solicitações canceladas (no entanto, os filtros de exceção irão obtê-los). Isso ocorre por design.

HttpConfiguration.Services.Add(typeof(IExceptionLogger), myWebApiExceptionLogger); 
Shaddy Zeineddine
fonte
Parece que o problema com essa abordagem é que o erro ainda aparece no tratamento de erros Global.asax ... Evento, embora não seja enviado ao manipulador de exceções
Ilya Chernomordik
14

Aqui está uma outra solução alternativa para esse problema. Basta adicionar um middleware OWIN personalizado no início do pipeline OWIN que captura OperationCanceledException:

#if !DEBUG
app.Use(async (ctx, next) =>
{
    try
    {
        await next();
    }
    catch (OperationCanceledException)
    {
    }
});
#endif
Huysentruitw
fonte
2
Eu estava recebendo este erro principalmente do contexto OWIN e este o direciona melhor
NitinSingh
3

Você pode tentar alterar o comportamento de manipulação de exceção de tarefa TPL padrão por meio de web.config:

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

Em seguida, tenha uma staticclasse (com um staticconstrutor) em seu aplicativo da web, que manipularia AppDomain.UnhandledException.

No entanto, parece que essa exceção está realmente sendo tratada em algum lugar dentro do tempo de execução da API da Web do ASP.NET , antes mesmo de você ter a chance de manipulá-la com seu código.

Nesse caso, você deve ser capaz de capturá-lo como uma exceção de 1ª chance, com AppDomain.CurrentDomain.FirstChanceException, aqui está como . Eu entendo que pode não ser o que você está procurando.

noseratio
fonte
1
Nenhum desses me permitiu lidar com a exceção também.
Bates Westmoreland
@BatesWestmoreland, nem mesmo FirstChanceException? Você já tentou lidar com isso com uma classe estática que persiste em solicitações HTTP?
noseratio
2
O problema que estou tentando resolver é pegar e ignorar essas exceções. Usar AppDomain.UnhandledExceptionou AppDomain.CurrentDomain.FirstChanceExceptionpode me permitir inspecionar a exceção, mas não capturar e ignorar. Não vi uma maneira de marcar essas exceções como tratadas usando qualquer uma dessas abordagens. Corrija-me se eu estiver errada.
Bates Westmoreland
2

Às vezes, recebo as mesmas 2 exceções em meu aplicativo Web API 2, mas posso capturá-las com o Application_Errormétodo em Global.asax.cse usando um filtro de exceção genérico .

O engraçado é, porém, eu prefiro não pegar essas exceções, porque eu sempre registro todas as exceções não tratadas que podem travar o aplicativo (essas 2, no entanto, são irrelevantes para mim e aparentemente não travam ou pelo menos não deveriam travar , mas posso estar errado). Suspeito que esses erros apareçam devido a alguma expiração de tempo limite ou cancelamento explícito do cliente, mas eu esperava que eles fossem tratados dentro da estrutura ASP.NET e não propagados fora dela como exceções não tratadas.

Gabriel S.
fonte
No meu caso, essas exceções ocorrem porque o navegador cancela a solicitação conforme o usuário navega para um novo url.
Bates Westmoreland
1
Entendo. No meu caso, as solicitações são emitidas por meio da WinHTTPAPI, não de um navegador.
Gabriel S.
2

Eu encontrei um pouco mais de detalhes sobre este erro. Existem 2 exceções possíveis que podem acontecer:

  1. OperationCanceledException
  2. TaskCanceledException

O primeiro acontece se a conexão for interrompida enquanto seu código no controlador é executado (ou possivelmente algum código de sistema ao redor disso também). Já o segundo ocorre se a conexão for interrompida enquanto a execução estiver dentro de um atributo (por exemplo AuthorizeAttribute).

Portanto, a solução alternativa fornecida ajuda a mitigar parcialmente a primeira exceção, mas não ajuda em nada com a segunda. No último caso, TaskCanceledExceptionocorre durante a base.SendAsyncprópria chamada, em vez de o token de cancelamento ser definido como verdadeiro.

Posso ver duas maneiras de resolver isso:

  1. Simplesmente ignorando ambas as exceções em global.asax. Então vem a pergunta se é possível ignorar de repente algo importante em vez disso?
  2. Fazendo um try / catch adicional no manipulador (embora não seja à prova de balas + ainda existe a possibilidade de TaskCanceledExceptionignorarmos ser aquele que desejamos 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;
    }
}

A única maneira que descobri que podemos tentar localizar as exceções erradas é verificando se stacktrace contém algum material Asp.Net. Não parece muito robusto.

PS É assim que eu filtro esses erros:

private static bool IsAspNetBugException(Exception exception)
{
    return
        (exception is TaskCanceledException || exception is OperationCanceledException) 
        &&
        exception.StackTrace.Contains("System.Web.HttpApplication.ExecuteStep");
}
Ilya Chernomordik
fonte
1
Em seu código sugerido, você cria a responsevariável dentro do try e a retorna fora do try. Isso não pode funcionar, pode? Além disso, onde você usa o IsAspNetBugException?
Schoof
1
Não, isso não pode funcionar, ele apenas precisa ser declarado fora do bloco try / catch e inicializado com algo como tarefa concluída. É apenas um exemplo de solução que não é à prova de balas de qualquer maneira. Quanto à outra questão, você a usa no manipulador Global.Asax OnError. Se você não usar esse para registrar suas mensagens, não precisa se preocupar de qualquer maneira. Se você fizer isso, este é um exemplo de como você filtra "não erros" do sistema.
Ilya Chernomordik
1

Temos recebido a mesma exceção, tentamos usar a solução alternativa de @dmatson, mas ainda obteríamos alguma exceção. Nós lidamos com isso até recentemente. Percebemos alguns logs do Windows crescendo a uma taxa alarmante.

Arquivos de erro localizados em: C: \ Windows \ System32 \ LogFiles \ HTTPERR

A maioria dos erros foram todos para "Timer_ConnectionIdle". Eu procurei e parecia que, embora a chamada da API da web tivesse sido concluída, a conexão ainda persistia por dois minutos após a conexão original.

Então percebi que deveríamos tentar fechar a conexão na resposta e ver o que acontece.

Eu adicionei response.Headers.ConnectionClose = true;ao SendAsync MessageHandler e pelo que posso dizer, os clientes estão fechando as conexões e não estamos tendo mais o problema.

Sei que não é a melhor solução, mas funciona no nosso caso. Também estou certo de que, em termos de desempenho, isso não é algo que você gostaria de fazer se sua API estiver recebendo várias chamadas do mesmo cliente consecutivas.

Michael Margala
fonte