ASP.NET Web API OperationCanceledException, коли браузер скасовує запит


119

Коли користувач завантажує сторінку, він робить один або кілька ajax запитів, які потрапляють на контролери ASP.NET Web API 2. Якщо користувач переходить на іншу сторінку, перш ніж ці запити ajax завершаться, браузер скасовує запити. Наш ELMAH HttpModule записує дві помилки для кожного скасованого запиту:

Помилка 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()

Помилка 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)

Дивлячись на стек-трек, я бачу, що звідси викидається виняток: https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Http.WebHost/HttpControllerHandler.cs# L413

Моє запитання: як я можу впоратись із ініціативами та проігнорувати ці винятки?

Схоже, він знаходиться поза кодом користувача ...

Примітки:

  • Я використовую ASP.NET Web API 2
  • Кінцеві точки Web API - це поєднання методів асинхронізації та неасинхронізації.
  • Незалежно від того, куди я додаю реєстрацію помилок, я не можу знайти виняток у коді користувача

1
Ми спостерігали однакові винятки (TaskCanceledException та OperationCanceledException) і з поточною версією бібліотек Katana.
Девід МакКлелленд

Я знайшов ще кілька деталей про те, коли трапляються обидва винятки, і зрозумів, що цей спосіб працює лише проти однієї з них. Ось декілька деталей: stackoverflow.com/questions/22157596/…
Ілля Чорномордик

Відповіді:


78

Це помилка в ASP.NET Web API 2, і, на жаль, я не думаю, що існує рішення, яке завжди матиме успіх. Ми подали помилку, щоб виправити її на боці.

Зрештою, проблема полягає в тому, що ми повертаємо скасовану задачу в ASP.NET в цьому випадку, і ASP.NET трактує скасовану задачу як необроблений виняток (вона записує проблему в журнал подій програми).

Тим часом ви можете спробувати щось на зразок коду нижче. Він додає обробник повідомлень верхнього рівня, який видаляє вміст, коли запускається маркер скасування. Якщо у відповіді немає вмісту, помилка не повинна бути запущена. Існує ще невелика ймовірність, що це може статися, оскільки клієнт міг відключитися відразу після того, як обробник повідомлень перевірить маркер скасування, але до того, як код веб-API вищого рівня зробить ту саму перевірку. Але я думаю, що це допоможе в більшості випадків.

Девід

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
Як оновлення, це захоплює деякі запити. Ми все ще бачимо досить багато в наших журналах. Дякую за обхід. Чекаємо виправлення.
Бейтс Вестморленд

2
@KiranChalla - можу підтвердити, що оновлення до 5.2.2 все ще має ці помилки.
KnightFox

2
Все-таки я отримую помилку. Я використовував вище пропозиції, будь-які інші підказки.
M2012

3
Коли я спробував вищевказану рекомендацію, я все-таки отримував Винятки, коли запит було скасовано ще до його передачі SendAsync(ви можете імітувати це, утримуючи F5в браузері URL-адресу, яка запитує ваш Api. Я вирішив цю проблему також додавши if (cancellationToken.IsCancellationRequested)галочку над викликом SendAsync. Тепер винятки більше не з’являються, коли браузер швидко скасовує запити.
seangwright

2
Я знайшов ще кілька деталей про те, коли трапляються обидва винятки, і зрозумів, що цей спосіб працює лише проти однієї з них. Ось деякі деталі: stackoverflow.com/a/51514604/1671558
Ілля Чорномордик

17

Реалізуючи реєстратор винятків для WebApi, рекомендується розширити System.Web.Http.ExceptionHandling.ExceptionLoggerклас, а не створювати ExceptionFilter. Внутрішні WebApi не будуть викликати метод журналу ExceptionLoggers для скасованих запитів (однак, фільтри виключень отримують їх). Це за дизайном.

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

Схоже, проблема такого підходу полягає в тому, що помилка все ще з’являється в обробці помилок Global.asax ... Подія, хоча вона не надіслана оброблювачу винятків
Іллі Чорномордику,

14

Ось інший спосіб вирішення цього питання. Просто додайте спеціальне програмне забезпечення OWIN на початку трубопроводу OWIN, який охоплює OperationCanceledException:

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

2
Я отримував цю помилку в основному з контексту OWIN, і цей орієнтується на неї краще
NitinSingh

3

Ви можете спробувати змінити поведінку поводження з винятком завдання TPL за допомогою web.config:

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

Потім у вашому веб-додатку є staticклас (з staticконструктором), який би обробляв AppDomain.UnhandledException.

Однак, виявляється, що цей виняток насправді обробляється десь у процесі виконання веб-API ASP.NET , перш ніж у вас навіть з’явиться шанс обробити його зі своїм кодом.

У цьому випадку ви маєте змогу сприймати це як виняток 1-го шансу AppDomain.CurrentDomain.FirstChanceException, ось , ось як . Я розумію, це може бути не тим, що ви шукаєте.


1
Жоден із них не дозволив мені обробляти виняток.
Бейтс Вестморленд

@BatesWestmoreland, навіть не FirstChanceException? Ви намагалися обробляти його статичним класом, який зберігається в HTTP-запитах?
носраціо

2
Проблему, яку я намагаюся вирішити, щоб наздогнати ці ігнори, ігнорувати. Використання AppDomain.UnhandledExceptionабо AppDomain.CurrentDomain.FirstChanceExceptionможе дозволити мені перевірити виняток, але не ловити та ігнорувати. Я не бачив способу позначити ці винятки як обробку за допомогою будь-якого з цих підходів. Виправте мене, якщо я помиляюся.
Бейтс Вестморленд

2

Іноді в моєму додатку Web API 2 я отримую ті самі 2 винятки, однак я можу впізнати їх Application_Errorметодом у Global.asax.csвикористанні загального фільтра виключень .

Найцікавіше, що, однак, я вважаю за краще не виловлювати ці винятки, тому що я завжди реєструю всі незроблені винятки, які можуть призвести до аварійного завершення роботи програми (однак ці 2 для мене не мають відношення і, мабуть, не мають або принаймні не повинні виходити з ладу. це, але я можу помилятися). Я підозрюю, що ці помилки з’являються через певний час закінчення або відмови від клієнта, але я б очікував, що вони будуть розглядатися в рамках ASP.NET, а не розповсюджуватися поза нею як непоправні винятки.


У моєму випадку ці винятки трапляються тому, що браузер скасовує запит, коли користувач переходить до нової URL-адреси.
Бейтс Вестморленд

1
Я бачу. У моєму випадку запити видаються через WinHTTPAPI, а не через браузер.
Габріель С.

2

Я знайшов трохи більше деталей щодо цієї помилки. Можливо, є 2 можливі винятки:

  1. OperationCanceledException
  2. TaskCanceledException

Перший трапляється, якщо зв’язок припинено, коли ваш код у контролері виконується (або, можливо, якийсь системний код навколо цього). У той час як другий відбувається, якщо з'єднання припинено, коли виконання знаходиться в атрибуті (наприклад AuthorizeAttribute).

Тож надане рішення допомагає частково пом'якшити перший виняток, воно не допомагає другому. В останньому випадку TaskCanceledExceptionтрапляється під час base.SendAsyncсамого виклику, а не, щоб маркер скасування був встановлений як істинний.

Я бачу два способи їх вирішення:

  1. Просто ігноруючи обидва винятки в global.asax. Тоді виникає питання, чи можна раптом ігнорувати щось важливе замість цього?
  2. Зробити додатковий спробу / ловити в обробнику (хоча це не є бронезахищеним + все ще існує можливість того, TaskCanceledExceptionщо ми ігноруємо, буде той, який ми хочемо увійти.

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

Єдиний спосіб, який я зрозумів, що ми можемо спробувати визначити неправильні винятки, перевіривши, чи містить стектрейк деякі матеріали Asp.Net. Це здається не дуже надійним.

PS Ось як я фільтрую ці помилки:

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

1
У запропонованому вами коді ви створюєте responseзмінну всередині спробу і повертаєте її поза спробою. Це не може працювати, чи не може? Також де ви використовуєте IsAspNetBugException?
Schoof

1
Ні, це не може працювати, його потрібно оголосити поза блоком спробувати / ловити, звичайно, і ініціалізувати щось подібне до виконаного завдання. Це лише приклад рішення, яке в будь-якому разі не є бронезахисним. Що стосується іншого питання, ви використовуєте його в обробнику Global.Asax OnError. Якщо ви не користуєтесь цим для реєстрації повідомлень, все одно не потрібно хвилюватися. Якщо ви це робите, це приклад того, як ви фільтруєте «не помилки» з системи.
Ілля Чорномордик

1

Ми отримали той самий виняток, ми намагалися використовувати вирішення @ dmatson, але все-таки ми отримаємо певний виняток. Ми цим займалися до недавнього часу. Ми помітили, що деякі журнали Windows ростуть з тривожною швидкістю.

Файли помилок, розташовані в: C: \ Windows \ System32 \ LogFiles \ HTTPERR

Більшість помилок були усі для "Timer_ConnectionIdle". Я обдивився навколо, і, здавалося, незважаючи на те, що веб-виклик api було закінчено, з'єднання все ще зберігалося дві хвилини після початкового з'єднання.

Тоді я подумав, що нам слід спробувати закрити зв’язок у відповідь і подивитися, що відбувається.

Я додав response.Headers.ConnectionClose = true;до SendAsync MessageHandler, і, як я можу сказати, клієнти закривають з'єднання, і ми вже не відчуваємо проблеми.

Я знаю, що це не найкраще рішення, але воно працює в нашому випадку. Я також впевнений, що це не те, що ви хочете робити, якщо ваш API отримує декілька дзвінків від одного і того ж клієнта.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.