Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Web API OperationCanceledException when browser cancels the request

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

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

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

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