Search code examples
c#.netasynchronousasp.net-web-apiasp.net-web-api2

Performing a server-side call using async/await pattern (error: TaskCanceledException)


I'm performing an AJAX call to my Web API, using the technology WebAPI.

The method that handles that request is the following:

[Route("RenderNotificationsPopup")]
[HttpGet]
public async Task<string> RenderNotificationsPopup(bool responsive = false)
{
    return await GetContentAsync(string.Format("RenderNotificationsPopup?responsive={0}", responsive ? 1 : 0));
}

This method calls GetContentAsync which peforms a server-side call using the class HttpClient.

private async Task<string> GetContentAsync(string urlSuffix)
{
        using (var client = new HttpClient())
        {
            return await client.GetStringAsync(string.Format(
                "{0}/Header/{1}",
                ConfigurationManager.AppSettings["GatewaysHttpRoot"],
                urlSuffix));
        }
}

Everything is working fine, except when users close the page while the AJAX query is pending a response from WebApi. In that case, I get this error:

Message: 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.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.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.d__0.MoveNext()

I can reproduce this issue myself by loading the page and closing it before I get the response of the AJAX query, like if leaving the page before while WebApi is processing the query is struggling itself.

I searched on the Web and saw that there is a bug related to that (ASP.NET Web API OperationCanceledException when browser cancels the request). Despite that, it apparently concerns an old version of WebApi package and I'm using the latest version.

Did I do a mistake in my code about the async/await pattern or is it always that nasty bug ?

Many thanks !

EDIT: I tried the workaround given in the SO post I linked (using the class CancelledTaskBugWorkaroundMessageHandler) and I still get the TaskCanceledException.

EDIT 2: Seems that this issue cannot be avoided. Is it possible to handle such exceptions in WebApi applications ?


Solution

  • I use my own derived class of ExceptionFilterAttribute in order to log the exception related to the WebApi application, overriding the method OnException.

    The proper way I found to handle the exception TaskCanceledException is to override OnExceptionAsync instead of OnException and rely on the property IsCancellationRequested to detect when the task is canceled.

    Here is the method I use as error handling:

    public override async Task OnExceptionAsync(HttpActionExecutedContext context, CancellationToken cancellationToken)
    {
        if (!cancellationToken.IsCancellationRequested)
        {
            //Handle domain specific exceptions here if any.
    
            //Handle all other Web Api (not all, but Web API specific only ) exceptions
            Logger.Write(context.Exception, "Email");
    
            context.Response = context.Request.CreateErrorResponse(HttpStatusCode.InternalServerError, "An unhandled exception was thrown.");
        }
    }
    

    Where the class Logger comes from the Enterprise Library Application Blocks.