API Web ASP.NET OperationCanceledException quando il browser annulla la richiesta


119

Quando un utente carica una pagina, effettua una o più richieste ajax, che colpiscono i controller ASP.NET Web API 2. Se l'utente naviga in un'altra pagina, prima che queste richieste ajax vengano completate, le richieste vengono annullate dal browser. Il nostro ELMAH HttpModule registra quindi due errori per ogni richiesta annullata:

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

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

Guardando lo stacktrace, vedo che l'eccezione viene generata da qui: https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Http.WebHost/HttpControllerHandler.cs# L413

La mia domanda è: come posso gestire e ignorare queste eccezioni?

Sembra essere al di fuori del codice utente ...

Appunti:

  • Sto usando ASP.NET Web API 2
  • Gli endpoint dell'API Web sono una combinazione di metodi asincroni e non asincroni.
  • Non importa dove aggiungo la registrazione degli errori, non sono in grado di rilevare l'eccezione nel codice utente

1
Abbiamo visto le stesse eccezioni (TaskCanceledException e OperationCanceledException) anche con la versione corrente delle librerie Katana.
David McClelland

Ho trovato alcuni dettagli in più su quando si verificano entrambe le eccezioni e ho capito che questa soluzione alternativa funziona solo contro una di esse. Ecco alcuni dettagli: stackoverflow.com/questions/22157596/…
Ilya Chernomordik

Risposte:


78

Questo è un bug nell'API Web ASP.NET 2 e, sfortunatamente, non credo che ci sia una soluzione alternativa che avrà sempre successo. Abbiamo presentato un bug per risolverlo dalla nostra parte.

In definitiva, il problema è che in questo caso restituiamo un'attività annullata ad ASP.NET e ASP.NET tratta un'attività annullata come un'eccezione non gestita (registra il problema nel registro eventi dell'applicazione).

Nel frattempo, potresti provare qualcosa di simile al codice qui sotto. Aggiunge un gestore di messaggi di primo livello che rimuove il contenuto quando viene attivato il token di annullamento. Se la risposta non ha contenuto, il bug non dovrebbe essere attivato. C'è ancora una piccola possibilità che possa accadere, perché il client potrebbe disconnettersi subito dopo che il gestore dei messaggi ha verificato il token di annullamento ma prima che il codice API Web di livello superiore esegua lo stesso controllo. Ma penso che aiuterà nella maggior parte dei casi.

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;
    }
}

2
Come aggiornamento, questo cattura alcune delle richieste. Ne vediamo ancora un bel po 'nei nostri log. Grazie per il work-around. In attesa di una soluzione.
Bates Westmoreland

2
@KiranChalla - Posso confermare che l'aggiornamento a 5.2.2 presenta ancora questi errori.
KnightFox

2
Continuo a ricevere l'errore. Ho usato il suggerimento sopra, qualsiasi altro indizio.
M2012

3
Quando ho provato la raccomandazione sopra, ho comunque ricevuto Eccezioni quando la richiesta era stata annullata anche prima che fosse passata a SendAsync(puoi simularlo tenendo premuto F5nel browser su un URL che fa richieste alla tua API. Ho risolto questo problema anche aggiungendo il if (cancellationToken.IsCancellationRequested)segno di spunta sopra la chiamata a SendAsync. Ora le eccezioni non vengono più visualizzate quando il browser annulla rapidamente le richieste.
seangwright

2
Ho trovato alcuni dettagli in più su quando si verificano entrambe le eccezioni e ho capito che questa soluzione alternativa funziona solo contro una di esse. Ecco alcuni dettagli: stackoverflow.com/a/51514604/1671558
Ilya Chernomordik

17

Quando si implementa un logger di eccezioni per WebApi, si consiglia di estendere la System.Web.Http.ExceptionHandling.ExceptionLoggerclasse invece di creare un ExceptionFilter. Gli interni di WebApi non chiameranno il metodo Log di ExceptionLoggers per le richieste annullate (tuttavia, i filtri di eccezione le otterranno). Questo è di progettazione.

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

Sembra che il problema con questo approccio sia che l'errore compare ancora nella gestione degli errori Global.asax ... Evento anche se non viene inviato al gestore delle eccezioni
Ilya Chernomordik

14

Ecco un'altra soluzione alternativa per questo problema. Basta aggiungere un middleware OWIN personalizzato all'inizio della pipeline OWIN che cattura OperationCanceledException:

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

2
Ho ricevuto questo errore principalmente dal contesto OWIN e questo lo indirizza meglio
NitinSingh

3

È possibile provare a modificare il comportamento di gestione delle eccezioni dell'attività TPL predefinito tramite web.config:

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

Quindi avere una staticclasse (con un staticcostruttore) nella tua app web, che gestirà AppDomain.UnhandledException.

Tuttavia, sembra che questa eccezione venga effettivamente gestita da qualche parte all'interno del runtime dell'API Web ASP.NET , prima ancora che tu abbia la possibilità di gestirla con il tuo codice.

In questo caso, dovresti essere in grado di catturarlo come prima eccezione di possibilità, con AppDomain.CurrentDomain.FirstChanceException, ecco come . Capisco che questo potrebbe non essere quello che stai cercando.


1
Nessuno di questi mi ha permesso di gestire l'eccezione.
Bates Westmoreland

@ BatesWestmoreland, nemmeno FirstChanceException? Hai provato a gestirlo con una classe statica che persiste tra le richieste HTTP?
noseratio

2
Il problema che sto cercando di risolvere per catturare e ignorare queste eccezioni. L'utilizzo di AppDomain.UnhandledExceptiono AppDomain.CurrentDomain.FirstChanceExceptionpuò consentirmi di ispezionare l'eccezione, ma non di catturarla e ignorarla. Non ho visto un modo per contrassegnare queste eccezioni come gestite utilizzando uno di questi approcci. Correggimi se sbaglio.
Bates Westmoreland

2

A volte ottengo le stesse 2 eccezioni nella mia applicazione Web API 2, tuttavia posso rilevarle con il Application_Errormetodo Global.asax.cse utilizzando un filtro di eccezione generico .

La cosa divertente è, però, che preferisco non rilevare queste eccezioni, perché registro sempre tutte le eccezioni non gestite che possono mandare in crash l'applicazione (queste 2, tuttavia, sono irrilevanti per me e apparentemente non lo fanno o almeno non dovrebbero andare in crash ma potrei sbagliarmi). Sospetto che questi errori si presentino a causa di una scadenza di timeout o di una cancellazione esplicita da parte del client, ma mi sarei aspettato che fossero trattati all'interno del framework ASP.NET e non propagati al di fuori di esso come eccezioni non gestite.


Nel mio caso, queste eccezioni si verificano perché il browser annulla la richiesta mentre l'utente naviga verso un nuovo URL.
Bates Westmoreland

1
Vedo. Nel mio caso, le richieste vengono emesse tramite l' WinHTTPAPI, non da un browser.
Gabriel S.

2

Ho trovato qualche dettaglio in più su questo errore. Ci sono 2 possibili eccezioni che possono verificarsi:

  1. OperationCanceledException
  2. TaskCanceledException

Il primo si verifica se la connessione viene interrotta durante l'esecuzione del codice nel controller (o forse anche del codice di sistema attorno a quello). Mentre il secondo accade se la connessione viene interrotta mentre l'esecuzione è all'interno di un attributo (es AuthorizeAttribute.).

Quindi la soluzione alternativa fornita aiuta a mitigare parzialmente la prima eccezione, non fa nulla per aiutare con la seconda. In quest'ultimo caso si TaskCanceledExceptionverifica durante la base.SendAsyncchiamata stessa piuttosto che il token di cancellazione viene impostato su true.

Posso vedere due modi per risolverli:

  1. Ignorando entrambe le eccezioni in global.asax. Poi viene la domanda se è possibile ignorare improvvisamente qualcosa di importante invece?
  2. Facendo un ulteriore tentativo / cattura nel gestore (sebbene non sia a prova di proiettile + c'è ancora la possibilità che TaskCanceledExceptionquello che ignoriamo sia quello che vogliamo registrare.

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;
    }
}

L'unico modo in cui ho capito che possiamo provare a individuare le eccezioni sbagliate è controllare se stacktrace contiene alcune cose Asp.Net. Non sembra però molto robusto.

PS Ecco come filtrare questi errori:

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

1
Nel codice suggerito, crei la responsevariabile all'interno del try e la restituisci al di fuori del try. Questo non può funzionare, vero? Inoltre, dove usi IsAspNetBugException?
Schoof

1
No, non può funzionare, deve solo essere dichiarato al di fuori del blocco try / catch ovviamente e inizializzato con qualcosa come attività completata. È solo un esempio della soluzione che non è comunque a prova di proiettile. Per quanto riguarda l'altra domanda, la usi nel gestore Global.Asax OnError. Se non lo usi per registrare i tuoi messaggi, non devi preoccuparti comunque. Se lo fai, questo è un esempio di come filtrare i "non errori" dal sistema.
Ilya Chernomordik

1

Abbiamo ricevuto la stessa eccezione, abbiamo provato a utilizzare la soluzione alternativa di @ dmatson, ma avremmo comunque ottenuto qualche eccezione. Ne abbiamo parlato fino a poco tempo fa. Abbiamo notato che alcuni registri di Windows crescono a una velocità allarmante.

File di errore situati in: C: \ Windows \ System32 \ LogFiles \ HTTPERR

La maggior parte degli errori erano tutti per "Timer_ConnectionIdle". Ho cercato in giro e sembrava che anche se la chiamata web api fosse terminata, la connessione persisteva ancora per due minuti dopo la connessione originale.

Quindi ho pensato che dovremmo provare a chiudere la connessione nella risposta e vedere cosa succede.

Ho aggiunto response.Headers.ConnectionClose = true; a SendAsync MessageHandler e da quello che posso dire i client stanno chiudendo le connessioni e non stiamo più riscontrando il problema.

So che questa non è la soluzione migliore, ma nel nostro caso funziona. Sono anche abbastanza sicuro che dal punto di vista delle prestazioni questo non è qualcosa che vorresti fare se la tua API riceve più chiamate dallo stesso client back-to-back.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.