skip to Main Content

There is a Web API (ASP.NET Core 3.1 / 6) which takes a while to run. Sometimes, the users close the tab before it finishes and cause ERR logs to be logged. The following is a simplified example.

[HttpGet(Name = "GetWeatherForecast")]
public async Task<IEnumerable<WeatherForecast>> Get(CancellationToken token)
{
    Console.WriteLine(token.IsCancellationRequested); // to reproduce the exception, set a breakpoint here.
        // open https://localhost:7057/WeatherForecast in a new tab, and close the tab when it breaks here
        // then token.IsCancellationRequested will be true
    await Task.Delay(1000, token); // Long run operation
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast { }).ToArray();
}

ERR logs with the following stack trace were logged,

fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]

An unhandled exception has occurred while executing the request.

System.Threading.Tasks.TaskCanceledException: A task was canceled.

at WebApplication2.Controllers.WeatherForecastController.Get(CancellationToken token) in C:sourcereposWebApplication2WebApplication2ControllersWeatherForecastController.cs:line 25
at lambda_method5(Closure , Object )
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

I can suppress the exception by catching the exception for all API. It’s too much to modify every method.

[HttpGet(Name = "GetWeatherForecast")]
public async Task<IEnumerable<WeatherForecast>> Get(CancellationToken token)
{
    Console.WriteLine(token.IsCancellationRequested);

    try
    {
        await Task.Delay(1000, token); // Long run operation
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast{ }).ToArray();
    }
    catch (TaskCanceledException)
    {
        return Enumerable.Empty<WeatherForecast>();
    }
}

Is there a way to suppress all TaskCanceledException exceptions? It should be quite common for users to close the web pages which call long run APIs, what’s the best approach to handle this?

2

Answers


  1. You could add a middleware on startup that intercepts any TaskCanceledException.

    On startup:

    app.UseMiddleware<TaskCanceledMiddleware>();
    

    And create a class for handling those exceptions:

    public class TaskCanceledMiddleware
    {
        private readonly RequestDelegate _next;
    
        public TaskCanceledMiddleware(RequestDelegate next)
        {
            _next = next;
        }
    
        public async Task InvokeAsync(HttpContext context)
        {
            try
            {
                await _next(context);
            }
            catch (TaskCanceledException)
            {
                // Set StatusCode 499 Client Closed Request
                context.Response.StatusCode = 499;
            }
        }
    }
    

    This middleware will always be called for every endpoint. If you don’t like this and you want to intercept the TaskCanceledException only on some specific endpoints you could also create an Exception filter.

    You just need to create a new attribute class that extends Microsoft.AspNetCore.Mvc.Filters.ExceptionFilterAttribute and then override OnException or OnExceptionAsync.

    After that you will have an action filter that you can put as an attribute of your endpoints or your controllers that handles the exceptions raised in the current call.

    You can read more about it here: https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-6.0#exception-filters

    Login or Signup to reply.
  2. You can use ExceptionFilters or creating a ErrorHandlerMiddleware.

    The ErrorHandlerMiddleware which handle TaskCanceledException can look like this:

    public class ErrorHandlerMiddleware
    {
        private readonly RequestDelegate _next;
    
        public ErrorHandlerMiddleware(RequestDelegate next)
        {
            _next = next;
        }
    
        public async Task Invoke(HttpContext context)
        {
            try
            {
                await _next(context);
            }
            catch (Exception error)
            {
                var response = context.Response;
                response.ContentType = "application/json";
    
                switch(error)
                {
                    case TaskCanceledExceptione:
                        // custom application error
                        // Non-Standard status code - Client Closed Request
                        response.StatusCode = 499;
                        break;
                    default:
                        // unhandled error
                        response.StatusCode = (int)HttpStatusCode.InternalServerError;
                        break;
                }
    
                var result = JsonSerializer.Serialize(new { message = error?.Message });
                await response.WriteAsync(result);
            }
        }
    }
    

    And add the Middleware:

    app.UseMiddleware<ErrorHandlerMiddleware>();
    

    Here is a detailed example how to create a ErrorHandlerMiddleware.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search