ASP.NET Web API OperationCanceledException, gdy przeglądarka anuluje żądanie

119

Gdy użytkownik ładuje stronę, wysyła jedno lub więcej żądań ajax, które trafiają do kontrolerów ASP.NET Web API 2. Jeśli użytkownik przejdzie na inną stronę, przed zakończeniem tych żądań ajax, żądania są anulowane przez przeglądarkę. Nasz ELMAH HttpModule następnie rejestruje dwa błędy dla każdego anulowanego żądania:

Błąd 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()

Błąd 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)

Patrząc na stacktrace, widzę, że wyjątek jest wyrzucany stąd: https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Http.WebHost/HttpControllerHandler.cs# L413

Moje pytanie brzmi: jak mogę obsłużyć i zignorować te wyjątki?

Wygląda na to, że jest poza kodem użytkownika ...

Uwagi:

  • Używam ASP.NET Web API 2
  • Punkty końcowe interfejsu API sieci Web to połączenie metod asynchronicznych i innych niż asynchroniczne.
  • Bez względu na to, gdzie dodam rejestrowanie błędów, nie mogę złapać wyjątku w kodzie użytkownika
Bates Westmoreland
źródło
1
Widzieliśmy te same wyjątki (TaskCanceledException i OperationCanceledException) z bieżącą wersją bibliotek Katana.
David McClelland
Znalazłem więcej szczegółów na temat tego, kiedy występują oba wyjątki, i odkryłem, że to obejście działa tylko w przypadku jednego z nich. Oto kilka szczegółów: stackoverflow.com/questions/22157596/ ...
Ilya Chernomordik

Odpowiedzi:

78

Jest to błąd w ASP.NET Web API 2 i niestety nie sądzę, aby można było obejść ten problem, który zawsze się powiedzie. Zgłosiliśmy błąd, aby go naprawić po naszej stronie.

Ostatecznie problem polega na tym, że w tym przypadku zwracamy anulowane zadanie do ASP.NET, a ASP.NET traktuje anulowane zadanie jak nieobsługiwany wyjątek (rejestruje problem w dzienniku zdarzeń aplikacji).

W międzyczasie możesz spróbować czegoś podobnego do poniższego kodu. Dodaje moduł obsługi komunikatów najwyższego poziomu, który usuwa zawartość po uruchomieniu tokenu anulowania. Jeśli odpowiedź nie zawiera treści, błąd nie powinien zostać wywołany. Nadal istnieje niewielka możliwość, że może się to zdarzyć, ponieważ klient może się rozłączyć zaraz po sprawdzeniu tokenu anulowania przez program obsługi komunikatów, ale zanim kod interfejsu API sieci Web wyższego poziomu wykona to samo sprawdzenie. Ale myślę, że w większości przypadków pomoże.

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
źródło
2
Jako aktualizacja obejmuje to niektóre żądania. Nadal widzimy sporo w naszych dziennikach. Dzięki za obejście problemu. Nie mogę się doczekać poprawki.
Bates Westmoreland
2
@KiranChalla - mogę potwierdzić, że aktualizacja do 5.2.2 nadal zawiera te błędy.
KnightFox
2
Nadal otrzymuję błąd. Skorzystałem z powyższej sugestii, innych wskazówek.
M2012
3
Kiedy wypróbowałem powyższe zalecenie, nadal otrzymywałem Wyjątki, gdy żądanie zostało anulowane jeszcze przed przekazaniem do SendAsync(możesz to zasymulować, przytrzymując F5w przeglądarce adres URL, który wysyła żądania do Twojego interfejsu API. Rozwiązałem ten problem również dodanie if (cancellationToken.IsCancellationRequested)czeku nad wezwaniem do SendAsync. Teraz wyjątki nie są już wyświetlane, gdy przeglądarka szybko anuluje żądania.
seangwright
2
Znalazłem więcej szczegółów na temat tego, kiedy występują oba wyjątki, i odkryłem, że to obejście działa tylko w przypadku jednego z nich. Oto kilka szczegółów: stackoverflow.com/a/51514604/1671558
Ilya Chernomordik
17

Podczas implementowania rejestratora wyjątków dla WebApi zaleca się rozszerzenie System.Web.Http.ExceptionHandling.ExceptionLoggerklasy zamiast tworzenia ExceptionFilter. Elementy wewnętrzne WebApi nie będą wywoływać metody Log programu ExceptionLoggers w przypadku anulowanych żądań (jednak filtry wyjątków będą je pobierać). Jest to zgodne z projektem.

HttpConfiguration.Services.Add(typeof(IExceptionLogger), myWebApiExceptionLogger); 
Shaddy Zeineddine
źródło
Wygląda na to, że problem z tym podejściem polega na tym, że błąd nadal wyskakuje w obsłudze błędów Global.asax ... Zdarzenie, chociaż nie jest wysyłane do modułu obsługi wyjątków
Ilya Chernomordik
14

Oto inne obejście tego problemu. Po prostu dodaj niestandardowe oprogramowanie pośredniczące OWIN na początku potoku OWIN, które przechwytuje OperationCanceledException:

#if !DEBUG
app.Use(async (ctx, next) =>
{
    try
    {
        await next();
    }
    catch (OperationCanceledException)
    {
    }
});
#endif
huysentruitw
źródło
2
Otrzymałem ten błąd głównie z kontekstu OWIN, a ten jest lepiej
ukierunkowany
3

Możesz spróbować zmienić domyślne zachowanie obsługi wyjątków zadania TPL poprzez web.config:

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

Następnie miej staticklasę (z statickonstruktorem) w swojej aplikacji internetowej, która będzie obsługiwać AppDomain.UnhandledException.

Wygląda jednak na to, że ten wyjątek jest faktycznie obsługiwany gdzieś w środowisku wykonawczym ASP.NET Web API , zanim jeszcze będziesz miał szansę obsłużyć go za pomocą kodu.

W takim przypadku powinieneś być w stanie złapać go jako wyjątek pierwszej szansy, za pomocą AppDomain.CurrentDomain.FirstChanceException, oto jak . Rozumiem, że może to nie być to, czego szukasz.

noseratio
źródło
1
Żaden z nich również nie pozwolił mi obsłużyć wyjątku.
Bates Westmoreland,
@BatesWestmoreland, nawet nie FirstChanceException? Czy próbowałeś poradzić sobie z tym za pomocą klasy statycznej, która utrzymuje się w żądaniach HTTP?
noseratio
2
Problem, który próbuję rozwiązać, aby złapać i zignorować te wyjątki. Używanie AppDomain.UnhandledExceptionlub AppDomain.CurrentDomain.FirstChanceExceptionmoże pozwolić mi na sprawdzenie wyjątku, ale nie na przechwytywanie i ignorowanie. Nie widziałem sposobu, aby oznaczyć te wyjątki jako obsługiwane przy użyciu któregokolwiek z tych podejść. Popraw mnie, jeśli się mylę.
Bates Westmoreland
2

Czasami dostaję te same 2 wyjątki w mojej aplikacji Web API 2, jednak mogę je złapać za pomocą Application_Errormetody wi Global.asax.csużywając ogólnego filtru wyjątków .

Zabawne jest jednak to, że wolę nie łapać tych wyjątków, ponieważ zawsze rejestruję wszystkie nieobsłużone wyjątki, które mogą spowodować awarię aplikacji (te 2 są jednak dla mnie nieistotne i najwyraźniej nie mają lub przynajmniej nie powinny się zawieszać to, ale mogę się mylić). Podejrzewam, że te błędy pojawiają się z powodu upływu limitu czasu lub jawnego anulowania przez klienta, ale spodziewałbym się, że będą traktowane wewnątrz platformy ASP.NET i nie będą propagowane poza nią jako nieobsługiwane wyjątki.

Gabriel S.
źródło
W moim przypadku te wyjątki występują, ponieważ przeglądarka anuluje żądanie, gdy użytkownik przechodzi do nowego adresu URL.
Bates Westmoreland
1
Widzę. W moim przypadku żądania wysyłane są za pośrednictwem WinHTTPinterfejsu API, a nie przeglądarki.
Gabriel S.
2

Znalazłem trochę więcej szczegółów na temat tego błędu. Mogą wystąpić 2 możliwe wyjątki:

  1. OperationCanceledException
  2. TaskCanceledException

Pierwsza z nich ma miejsce, gdy połączenie zostanie zerwane podczas wykonywania kodu w kontrolerze (lub prawdopodobnie również kod systemowy wokół tego). Podczas gdy drugi ma miejsce, jeśli połączenie zostanie zerwane, gdy wykonanie znajduje się wewnątrz atrybutu (np AuthorizeAttribute.).

Tak więc podane obejście pomaga częściowo złagodzić pierwszy wyjątek, a nie pomaga w przypadku drugiego. W tym drugim przypadku TaskCanceledExceptionwystępuje podczas base.SendAsyncsamego wywołania, a token anulowania jest ustawiony na wartość true.

Widzę dwa sposoby rozwiązania tego problemu:

  1. Po prostu zignoruj ​​oba wyjątki w global.asax. Następnie pojawia się pytanie, czy zamiast tego można nagle zignorować coś ważnego?
  2. Wykonanie dodatkowej próby / złapania w module obsługi (chociaż nie jest to kuloodporne + nadal istnieje możliwość, TaskCanceledExceptionże to , co zignorujemy, będzie tym, które chcemy zarejestrować.

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

Jedynym sposobem, w jaki się zorientowałem, że możemy spróbować wskazać niewłaściwe wyjątki, jest sprawdzenie, czy stacktrace zawiera jakieś elementy Asp.Net. Nie wydaje się jednak zbyt mocny.

PS Tak odfiltrowuję te błędy:

private static bool IsAspNetBugException(Exception exception)
{
    return
        (exception is TaskCanceledException || exception is OperationCanceledException) 
        &&
        exception.StackTrace.Contains("System.Web.HttpApplication.ExecuteStep");
}
Ilya Chernomordik
źródło
1
W sugerowanym kodzie tworzysz responsezmienną wewnątrz try i zwracasz ją poza try. To niemożliwe, prawda? Gdzie również używasz wyjątku IsAspNetBugException?
Schoof,
1
Nie, to nie może zadziałać, po prostu trzeba to oczywiście zadeklarować poza blokiem try / catch i zainicjować czymś w rodzaju ukończonego zadania. To tylko przykład rozwiązania, które i tak nie jest kuloodporne. Jeśli chodzi o drugie pytanie, używasz go w module obsługi Global.Asax OnError. Jeśli nie używasz tego do rejestrowania wiadomości, i tak nie musisz się martwić. Jeśli to zrobisz, to jest przykład tego, jak odfiltrowujesz z systemu „nie błędy”.
Ilya Chernomordik
1

Otrzymaliśmy ten sam wyjątek, próbowaliśmy zastosować obejście @ dmatson, ale mimo to otrzymaliśmy jakiś wyjątek. Zajmowaliśmy się tym do niedawna. Zauważyliśmy, że niektóre dzienniki systemu Windows rosną w alarmującym tempie.

Pliki błędów zlokalizowane w: C: \ Windows \ System32 \ LogFiles \ HTTPERR

Większość błędów dotyczyła „Timer_ConnectionIdle”. Szukałem dookoła i wydawało się, że chociaż wywołanie interfejsu API sieci Web zostało zakończone, połączenie nadal trwało przez dwie minuty po pierwotnym połączeniu.

Wtedy doszedłem do wniosku, że powinniśmy spróbować zamknąć połączenie w odpowiedzi i zobaczyć, co się stanie.

Dodałem response.Headers.ConnectionClose = true;do SendAsync MessageHandler iz tego, co mogę powiedzieć, klienci zamykają połączenia i już nie mamy problemu.

Wiem, że to nie jest najlepsze rozwiązanie, ale w naszym przypadku działa. Jestem też prawie pewien, że pod względem wydajności nie jest to coś, co chciałbyś zrobić, jeśli twoje API otrzymuje wiele wywołań od tego samego klienta z powrotem do tyłu.

Michael Margala
źródło