Catch Exception and respond with custom message without skipping OutputFormatter

Viewed 31

I am developing WebAPI and want to catch all my ApiException custom exceptions and display WebAPI-friendly responses. The ApiException exception can be thrown from Action or Filter like IAuthorizationFilter or ActionFilterAttribute.

First I tried to use IExceptionFilter but later I found that the IExceptionFilter handles only exceptions thrown from Actions and not from other Filters.

public class ApiExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
    
        var exception = context.Exception;
    
        if (exception is not ApiException responseException)
        {
            responseException = new ApiException(ResponseMessageType.UnhandledException);
        }
    
        context.Result = new ObjectResult(new ResultMessageDto(responseException))
        {
            StatusCode = responseException.HttpStatusCode
        };
    }
    
}

The second approach that I found many suggest to use is the Middleware but this is not the correct way by WebAPI design.

    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 exception)
            {
                var response = context.Response;
                response.ContentType = "application/json";
    
                if (exception is not ApiException responseException)
                {
                    responseException = new ApiException(ResponseMessageType.UnhandledException);
                }
    
                response.StatusCode = responseException.HttpStatusCode;
                await response.WriteAsJsonAsync(new ResultMessageDto(responseException), new JsonSerializerOptions()
                {
                    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
                    PropertyNamingPolicy = null
                });
            }
    }
}

The Middleware exception handling skips WebAPI MVC OutputFormaters and responds only in JSON or what is set by the developer. This solution is bad by design because do not respect Accept header.

How to handle Exceptions in Actions and Filters without leaving MVC scope?

1 Answers

I feel it is best to handle in Middleware, Here is a sample code. I am using Json but over here, you can use any output formatter (even custom ones).

 public async Task InvokeAsync(HttpContext context, RequestDelegate next)
 {
    try
    {
        await next(context);
    }
    catch (Exception ex)
    {
         await HandleExceptionAsync(context, ex);
    }
}

private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
   int statusCode = StatusCodes.Status500InternalServerError;
   string exceptionType, exceptionDetail;
   context.Response.ContentType = "application/json";

   switch (exception)
   {
    case ArgumentNullException:
          exceptionType = "Argument Null Exception";
          exceptionDetail = exception.Message;
          break;
      case ApiException:
          exceptionType = "API Exception";
          exceptionDetail = exception.Message;
          break;
   default:
          exceptionType = "Unhandled"
          exceptionDetail = "Something went wrong";
          break;
   }

    var problemDetails = new Microsoft.AspNetCore.Mvc.ProblemDetails
    {
        Status = statusCode,
        Type = exceptionType,
        Title = "An error has occurred within the PromComm Application",
         Detail = exceptionDetail,
         Instance = context.Request.Path
     };

    // You can use any formatter here even with custom messages
     var problemDetailsJson = System.Text.Json.JsonSerializer.Serialize(problemDetails);

     context.Response.StatusCode = statusCode;
     await context.Response.WriteAsync(problemDetailsJson);
}
Related