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)

Глядя на stacktrace, я вижу, что исключение генерируется отсюда: https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Http.WebHost/HttpControllerHandler.cs# L413

Мой вопрос: как я могу обрабатывать и игнорировать эти исключения?

Похоже, что это вне кода пользователя ...

Ноты:

  • Я использую веб-API ASP.NET 2
  • Конечные точки веб-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;
    }
}
dmatson
источник
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
huysentruitw
источник
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, вот как . Я понимаю, что это может быть не то, что вы ищете.

noseratio
источник
1
Ни один из них не позволил мне обработать исключение.
Bates Westmoreland
@BatesWestmoreland, даже нет FirstChanceException? Вы пробовали обрабатывать его с помощью статического класса, который сохраняется в HTTP-запросах?
Носератио 05
2
Проблема, которую я пытаюсь решить, это поймать и игнорировать эти исключения. Использование AppDomain.UnhandledExceptionили AppDomain.CurrentDomain.FirstChanceExceptionможет позволить мне проверить исключение, но не поймать и игнорировать. Я не видел способа пометить эти исключения как обработанные с использованием любого из этих подходов. Поправьте меня, если я ошибаюсь.
Bates Westmoreland
2

Иногда я получаю те же 2 исключения в моем приложении Web API 2, однако я могу поймать их с помощью Application_Errorметода Global.asax.csи общего фильтра исключений .

Однако забавно то, что я предпочитаю не улавливать эти исключения, потому что я всегда регистрирую все необработанные исключения, которые могут привести к сбою приложения (эти 2, однако, для меня не имеют значения и, по-видимому, не приводят к сбою или, по крайней мере, не должны сбой это, но я могу ошибаться). Я подозреваю, что эти ошибки появляются из-за истечения тайм-аута или явной отмены со стороны клиента, но я ожидал, что они будут обрабатываться внутри платформы ASP.NET, а не распространяться за ее пределы как необработанные исключения.

Габриэль С.
источник
В моем случае эти исключения возникают из-за того, что браузер отменяет запрос, когда пользователь переходит на новый URL-адрес.
Bates Westmoreland
1
Понимаю. В моем случае запросы отправляются через WinHTTPAPI, а не из браузера.
Габриэль С.
2

Я нашел более подробную информацию об этой ошибке. Возможны 2 исключения:

  1. OperationCanceledException
  2. TaskCanceledException

Первый происходит, если соединение разрывается во время выполнения вашего кода в контроллере (или, возможно, некоторого системного кода вокруг этого). В то время как второй происходит, если соединение разрывается, когда выполнение находится внутри атрибута (например AuthorizeAttribute).

Таким образом, предоставленный обходной путь помогает частично смягчить первое исключение, но не помогает со вторым. В последнем случае это TaskCanceledExceptionпроисходит во время base.SendAsyncсамого вызова, а для токена отмены устанавливается значение true.

Я вижу два пути решения этих проблем:

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

Единственный способ, которым я понял, что мы можем попытаться определить неправильные исключения, - это проверить, содержит ли stacktrace какой-либо материал 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
Нет, это не сработает, его просто нужно объявить вне блока try / catch, конечно, и инициализировать чем-то вроде выполненной задачи. Это просто пример решения, которое в любом случае не является пуленепробиваемым. Что касается другого вопроса, вы используете его в обработчике Global.Asax OnError. Если вы не используете его для регистрации сообщений, вам все равно не о чем беспокоиться. Если да, то это пример того, как вы отфильтровываете из системы «не ошибки».
Илья Черномордик
1

Мы получали то же исключение, мы пытались использовать обходной путь @dmatson, но мы все равно получали какое-то исключение. Мы занимались этим до недавнего времени. Мы заметили, что некоторые журналы Windows растут с угрожающей скоростью.

Файлы ошибок расположены в: C: \ Windows \ System32 \ LogFiles \ HTTPERR

Большинство ошибок было связано с "Timer_ConnectionIdle". Я поискал все вокруг, и мне показалось, что, хотя вызов веб-API был завершен, соединение все еще сохранялось в течение двух минут после исходного соединения.

Затем я решил, что мы должны попытаться закрыть соединение в ответе и посмотреть, что произойдет.

Я добавил response.Headers.ConnectionClose = true;в SendAsync MessageHandler и, насколько я могу судить, клиенты закрывают соединения, и мы больше не сталкиваемся с этой проблемой.

Я знаю, что это не лучшее решение, но в нашем случае оно работает. Я также уверен, что с точки зрения производительности это не то, что вы хотели бы делать, если ваш API получает несколько вызовов от одного и того же клиента подряд.

Майкл Маргала
источник