1

I have an application with both MVC and 'new' ApiController endpoints in ASP.NET Core 2.2 co-existing together.

Prior to adding the API endpoints, I have been using a global exception handler registered as middleware using app.UseExceptionHandler((x) => { ... } which would redirect to an error page.

Of course, that does not work for an API response and I would like to return an ObjectResult (negotiated) 500 result with a ProblemDetails formatted result.

The problem is, I'm not sure how to reliably determine in my 'UseExceptionHandler' lambda if I am dealing with an MVC or a API request. I could use some kind of request URL matching (eg. /api/... prefix) but I would like a more robust solution that won't come back to bite me in the future.

Rough psuedo-code version of what I'm trying to implement is:

app.UseExceptionHandler(x =>
{
    x.Run(async context =>
    {
        // extract the exception that was thrown
        var ex = context.Features.Get<IExceptionHandlerFeature>()?.Error;

        try
        {
            // generically handle the exception regardless of what our response needs to look like by logging it
            // NOTE: ExceptionHandlerMiddleware itself will log the exception

            // TODO: need to find a way to see if we have run with negotiation turned on (in which case we are API not MVC!! see below extensions for clues?)
            // TODO: ... could just use "/api/" prefix but that seems rubbish
            if (true)
            {
                // return a 500 with object (in RFC 7807 form) negotiated to the right content type (eg. json)
            }
            else
            {
                // otherwise, we handle the response as a 500 error page redirect
            }
        }
        catch (Exception exofex)
        {
            // NOTE: absolutely terrible if we get into here
            log.Fatal($"Unhandled exception in global error handler!", exofex);
            log.Fatal($"Handling exception: ", ex);
        }
    });
});
}

Any ideas?

Cheers!

Nashical
  • 17
  • 1
Kieran Benton
  • 8,739
  • 12
  • 53
  • 77
  • IMO, it's enough and conveniece to check request path to see whether it is a web api request. – Edward Mar 22 '19 at 02:09

3 Answers3

1

This might be a bit different than what you expect, but you could just check if the request is an AJAX request.

You can use this extension:

public static class HttpRequestExtensions
{
    public static bool IsAjaxRequest(this HttpRequest request)
    {
        if (request == null)
            throw new ArgumentNullException(nameof(request));

        if (request.Headers == null)
            return false;

        return request.Headers["X-Requested-With"] == "XMLHttpRequest";
    }
}

And then middleware with an invoke method that looks like:

public async Task Invoke(HttpContext context)
{
    if (context.Request.IsAjaxRequest())
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
             //Handle the exception
             await HandleExceptionAsync(context, ex);
        }
    }
    else
    {
        await _next(context);
    }
}

private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
    //you can do more complex logic here, but a basic example would be:
    var result = JsonConvert.SerializeObject(new { error = "An unexpected error occurred." });
    context.Response.ContentType = "application/json";
    context.Response.StatusCode = 500;
    return context.Response.WriteAsync(result);
}

see this SO answer for a more detailed version.

John-Luke Laue
  • 3,736
  • 3
  • 32
  • 60
1

If you want to check whether the request is routed to ApiController, you could try IExceptionFilter to hanlde the exceptions.

public class CustomExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        if (IsApi(context))
        {
            HttpStatusCode status = HttpStatusCode.InternalServerError;
            var message = context.Result;

            //You can enable logging error

            context.ExceptionHandled = true;
            HttpResponse response = context.HttpContext.Response;
            response.StatusCode = (int)status;
            response.ContentType = "application/json";
            context.Result = new ObjectResult(new { ErrorMsg = message });
        }
        else
        {

        }
    }
    private bool IsApi(ExceptionContext context)
    {
        var controllerActionDesc = context.ActionDescriptor as ControllerActionDescriptor;
        var attribute = controllerActionDesc
                        .ControllerTypeInfo
                        .CustomAttributes
                        .FirstOrDefault(c => c.AttributeType == typeof(ApiControllerAttribute));
        return attribute == null ? false : true;
    }
}
Edward
  • 28,296
  • 11
  • 76
  • 121
0

Thanks to all of the advice from others, but I have realised after some more thought and ideas from here that my approach wasn't right in the first place - and that I should be handling most exceptions locally in the controller and responding from there.

I have basically kept my error handling middleware the same as if it was handling MVC unhandled exceptions. The client will get a 500 with a HTML response, but at that point there isn't much the client can do anyway so no harm.

Thanks for your help!

Kieran Benton
  • 8,739
  • 12
  • 53
  • 77