129

When a user loads a page, it makes one or more ajax requests, which hit ASP.NET Web API 2 controllers. If the user navigates to another page, before these ajax requests complete, the requests are canceled by the browser. Our ELMAH HttpModule then logs two errors for each canceled request:

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

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

Looking at the stacktrace, I see that the exception is being thrown from here: https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Http.WebHost/HttpControllerHandler.cs#L413

My question is: How can I handle and ignore these exceptions?

It appears to be outside of user code...

Notes:

  • I am using ASP.NET Web API 2
  • The Web API endpoints are a mix of async and non-async methods.
  • No matter where I add error logging, I am unable to catch the exception in user code
Bates Westmoreland
  • 1,393
  • 2
  • 10
  • 10
  • 1
    We have seen the same exceptions (TaskCanceledException and OperationCanceledException) with the current version of the Katana libraries too. – David McClelland Jul 18 '17 at 15:27
  • I have found some more details on when both exception happen and figured out that this workaround only works against one of them. Here are some details: https://stackoverflow.com/questions/22157596/asp-net-web-api-operationcanceledexception-when-browser-cancels-the-request/51514604#51514604 – Ilya Chernomordik Aug 08 '18 at 11:47

10 Answers10

79

This is a bug in ASP.NET Web API 2 and unfortunately, I don't think there's a workaround that will always succeed. We filed a bug to fix it on our side.

Ultimately, the problem is that we return a cancelled task to ASP.NET in this case, and ASP.NET treats a cancelled task like an unhandled exception (it logs the problem in the Application event log).

In the meantime, you could try something like the code below. It adds a top-level message handler that removes the content when the cancellation token fires. If the response has no content, the bug shouldn't be triggered. There's still a small possibility it could happen, because the client could disconnect right after the message handler checks the cancellation token but before the higher-level Web API code does the same check. But I think it will help in most cases.

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
  • 6,035
  • 1
  • 23
  • 23
  • 2
    As an update, this does capture some of the requests. We still see quite a few in our logs. Thanks for the work-around. Looking forward to a fix. – Bates Westmoreland Apr 11 '14 at 18:34
  • 1
    I used this workaround for quite a while, and it seemed to work great. Recently - I think associated with an upgrade to ASP.NET MVC 5.2 - this error started showing up in my logs again. – Ken Smith Jul 15 '14 at 04:39
  • @KenSmith: Do you still see this error after upgrading to 5.2.2? – Kiran Sep 15 '14 at 15:09
  • 2
    @KiranChalla - I can confirm that the upgrade to 5.2.2 still has these errors. – KnightFox Feb 09 '15 at 15:26
  • 1
    @KnightFox I am also seeing these errors in my application sporadically, even though I have the latest version of Web API 2. – Nate Barbettini Apr 11 '15 at 14:45
  • 3
    As a temporary fix and in order to stop business from panicking can I add below code into my ElmahExceptionFilter?? ``public override void OnException(HttpActionExecutedContext context) { if (null != context.Exception && !(context.Exception is TaskCanceledException || context.Exception is OperationCanceledException)) { Elmah.ErrorSignal.FromCurrentContext().Raise(context.Exception); } base.OnException(context); }`` – Ravi Apr 30 '15 at 15:30
  • 2
    Still I'm getting the error. I've used above suggestion, any other clues. – M2012 Dec 08 '15 at 09:51
  • I updated ASP.NET MVC 5.2 and i added workaround also still i am getting "Task Cancelled Exception". – Mohan Gopi Jan 28 '16 at 05:28
  • after patch this workaround, still meet those exceptions under webapi 5.2.2. – Bill.Zhuang Feb 01 '16 at 08:24
  • @MohanGopi please see my solution below. I ran into issues using the accepted answer and I found out it stops working in later versions of WebAPI – Shaddy Zeineddine May 23 '16 at 18:42
  • In my case ,client is retrofit (okhttp) , an android network library , not a browser . Can you guide me to find the root of this problem please? – sepehr Oct 23 '16 at 08:29
  • 3
    When I tried the recommendation above, I still received Exceptions when the request had been canceled even before it was passed to `SendAsync` (you can simulate this by holding down `F5` in the browser on a url that makes requests to your Api. I solved this issue by also adding the `if (cancellationToken.IsCancellationRequested)` check above the call to `SendAsync`. Now the exceptions no longer show up when the browser quickly cancels requests. – seangwright Oct 04 '17 at 18:10
  • 1
    Is there a link to the github bug? Anyone knows if this is already fixed in `5.2.6`? – JobaDiniz Jun 12 '18 at 16:58
  • 1
    @JobaDiniz [Bug on gihub](https://github.com/aspnet/AspNetWebStack/issues/8) I believe they are not going to fix it. – vsevolod Jun 27 '18 at 04:05
  • 3
    I have found some more details on when both exception happen and figured out that this workaround only works against one of them. Here are some details: https://stackoverflow.com/a/51514604/1671558 – Ilya Chernomordik Jul 25 '18 at 08:49
  • Shouldn't the if be before you call the base.SendAsync? This doesn't solve the issue for us at all. – Schoof Oct 25 '18 at 13:53
  • With this in place, will my requests still be processed correctly in case of where a cancellation token is sent? – Ciaran Gallagher Jun 11 '19 at 13:39
  • I tried the suggestion by @seangwright, but now my code doesn't throw exceptions but also doesn't process the request. Is there any way to ignore the cancellation request? – Ciaran Gallagher Jun 11 '19 at 13:54
  • 2
    For those who want to trigger this error to see how it behaves, the best way to do so is the following. Click *Pause* while debugging. Make a request to your API then abort it. *Resume* your Web API app. You will get the OperationCanceledException. – jsgoupil Jul 03 '19 at 16:53
17

When implementing an exception logger for WebApi, it is recommend to extend the System.Web.Http.ExceptionHandling.ExceptionLogger class rather than creating an ExceptionFilter. The WebApi internals will not call the Log method of ExceptionLoggers for canceled requests (however, exception filters will get them). This is by design.

HttpConfiguration.Services.Add(typeof(IExceptionLogger), myWebApiExceptionLogger); 
Shaddy Zeineddine
  • 467
  • 1
  • 5
  • 8
  • It seems that the problem with this approach is that the error still pops up in the Global.asax error handling... Event though it's not sent to the exception handler – Ilya Chernomordik Jul 25 '18 at 07:59
15

Here's an other workaround for this issue. Just add a custom OWIN middleware at the beginning of the OWIN pipeline that catches the OperationCanceledException:

#if !DEBUG
app.Use(async (ctx, next) =>
{
    try
    {
        await next();
    }
    catch (OperationCanceledException)
    {
    }
});
#endif
huysentruitw
  • 27,376
  • 9
  • 90
  • 133
9

I have found a bit more details on this error. There are 2 possible exceptions that can happen:

  1. OperationCanceledException
  2. TaskCanceledException

The first one happens if connection is dropped while your code in controller executes (or possibly some system code around that as well). While the second one happens if the connection is dropped while the execution is inside an attribute (e.g. AuthorizeAttribute).

So the provided workaround helps to mitigate partially the first exception, it does nothing to help with the second. In the latter case the TaskCanceledException occurs during base.SendAsync call itself rather that cancellation token being set to true.

I can see two ways of solving these:

  1. Just ignoring both exceptions in global.asax. Then comes the question if it's possible to suddenly ignore something important instead?
  2. Doing an additional try/catch in the handler (though it's not bulletproof + there is still possibility that TaskCanceledException that we ignore will be a one we want to log.

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

The only way I figured out we can try to pinpoint the wrong exceptions is by checking if stacktrace contains some Asp.Net stuff. Does not seem very robust though.

P.S. This is how I filter these errors out:

private static bool IsAspNetBugException(Exception exception)
{
    return
        (exception is TaskCanceledException || exception is OperationCanceledException) 
        &&
        exception.StackTrace.Contains("System.Web.HttpApplication.ExecuteStep");
}
Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207
  • 1
    In your suggested code, you create the `response` variable inside the try and return it outside of the try. That can't possible work can it? Also where do you use the IsAspNetBugException? – Schoof Oct 19 '18 at 14:06
  • 1
    No, that can't work, it's just has to be declared outside of the try/catch block of course and initialized with something like completed task. It's just an example of the solution that is not bulletproof anyway. As for the other question you use it in the Global.Asax OnError handler. If you don't use that one to log your messages, you don't need to worry anyway. If you do, this is an example of how you filter out "non errors" from system. – Ilya Chernomordik Oct 29 '18 at 13:04
3

You could try changing the default TPL task exception handling behavior through web.config:

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

Then have a static class (with a static constructor) in your web app, which would handle AppDomain.UnhandledException.

However, it appears that this exception is actually getting handled somewhere inside ASP.NET Web API runtime, before you even have a chance to handle it with your code.

In this case, you should be able to catch it as a 1st chance exception, with AppDomain.CurrentDomain.FirstChanceException, here is how. I understand this may not be what you are looking for.

noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 1
    Neither of those allowed me to handle the exception either. – Bates Westmoreland Mar 05 '14 at 22:09
  • @BatesWestmoreland, not even `FirstChanceException`? Have you tried handling it with a static class which persists across HTTP requests? – noseratio Mar 05 '14 at 23:10
  • 2
    The problem I am trying to solve it to catch, and ignore these exceptions. Using `AppDomain.UnhandledException` or `AppDomain.CurrentDomain.FirstChanceException` may allow me to inspect the exception, but not catch and ignore. I didn't see a way to mark these exceptions as handled using either of these approaches. Correct me if I am wrong. – Bates Westmoreland Mar 06 '14 at 19:40
  • Throwing unobserved task exceptions is a bad idea; there's a reason it's off by default. Every method with an `async void` signature would throw one, as well as any Tasks at all that get garbage collected without having observed their result. Furthermore, that's not even related to unhandled TaskCancelledExceptions. If a TaskCancelledException is being thrown, the result of the Task (cancelled) has already been observed. – Triynko Feb 18 '22 at 18:25
2

I sometimes get the same 2 exceptions in my Web API 2 application, however i can catch them with the Application_Error method in Global.asax.cs and using a generic exception filter.

The funny thing is, though, i prefer not to catch these exceptions, because i always log all the unhandled exceptions that can crash the application (these 2, however, are irrelevant for me and apparently don't or at least shouldn't crash it, but i may be wrong). I suspect these errors show up due to some timeout expiration or explicit cancellation from the client, but i would have expected them to be treated inside the ASP.NET framework and not propagated outside of it as unhandled exceptions.

Gabriel S.
  • 1,347
  • 11
  • 31
2

The OP mentioned the desire to ignore System.OperationCanceledException within ELMAH and even provides a link in the right direction. ELMAH has come a long way since the original post and it provides a rich capability to do exactly what the OP is requesting. Please see this page (still being completed) which outlines the programmatic and declarative (configuration-based) approaches.

My personal favorite is the declarative approach made directly in the Web.config. Follow the guide linked above to learn how to set up your Web.config for configuration-based ELMAH exception filtering. To specifically filter-out System.OperationCanceledException, you would use the is-type assertion as such:

<configuration>
  ...
  <elmah>
  ...
    <errorFilter>
      <test>
        <or>
          ...
          <is-type binding="BaseException" type="System.OperationCanceledException" />
          ...
        </or>
      </test>
    </errorFilter>
  </elmah>
  ...
</configuration>
Jasel
  • 535
  • 8
  • 14
1

We have been receiving the same exception, we tried to use @dmatson's workaround, but we'd still get some exception through. We dealt with it until recently. We noticed some Windows logs growing at an alarming rate.

Error files located in: C:\Windows\System32\LogFiles\HTTPERR

Most of the errors's were all for "Timer_ConnectionIdle". I searched around and it seemed that even though the web api call was finished, the connection still persisted for two minutes past the original connection.

I then figured we should try to close the connection in the response and see what happens.

I added response.Headers.ConnectionClose = true; to the SendAsync MessageHandler and from what I can tell the clients are closing the connections and we aren't experiencing the issue any more.

I know this isn't the best solution, but it works in our case. I'm also i'm pretty sure performance-wise this isn't something you'd want to do if your API is getting multiple calls from the same client back-to-back.

0

In case you are using sentry SDK you can ignore it in the options of UseSentry() like this:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder
                    .UseSentry(options =>
                    {
                        options.AddExceptionFilterForType<OperationCanceledException>();
                    })
                    .UseContentRoot(Directory.GetCurrentDirectory())
                    .UseStartup<Startup>();
            });
}

Link to the docs: https://sentry-docs-git-sentry-ruby-40.sentry.dev/platforms/dotnet/guides/aspnetcore/ignoring-exceptions

According to the docs:

SentrySdk.Init(o => o.AddExceptionFilterForType<OperationCanceledException>());

Should also work with newer versions of ASP.NET WebApi.

CodingYourLife
  • 7,172
  • 5
  • 55
  • 69
0

This solution is derived from a solution provided above by @dmatson and other supporting members and with this code I was able to resolve my issue.

The Code:

//add handler to config
    config.MessageHandlers.Add(new CancelledTaskBugWorkaroundMessageHandler());
 
    // create handler
    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.OK);
                }
                return response;

            }
            catch (Exception ex)
            {
                if(IsAspNetBugException(ex))
                    return new HttpResponseMessage(HttpStatusCode.OK);
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }


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

Note: You can return response message of your choice i.e Internal server or OK personally, I prefer OK if the issue is not from code, however if it's just an operation that was cancelled from an user and we can't do anything about it other than just log the error.

cokeman19
  • 2,405
  • 1
  • 25
  • 40