5

I'm having troubles handling all types of errors in ASP.NET WebAPI.

I've successfully handled exceptions thrown inside my action methods using an ExceptionFilter and 404 errors for invalid routes, invalid controller or action name. However, I'm struggling to handle the error where the controller and action are both found, but the parameters for model binding are incorrect types.

Here's my action, which is routed to /api/users/{id}.

[HttpGet]
public virtual TPoco Get(long id)
{
    ...
}

If I request the URL /api/users/notinteger, I get a 400 Bad Request error that is handled outside of my code:

{
Message: "The request is invalid.",
MessageDetail: "The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int64' for method '___ Get(Int64)' in '___'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter."
}

How can I intercept this error and respond with my own error message? Preferably not in the controller itself as I'd like to handle several controllers in the same way.

I've tried using global.asax.cs's Application_Error event as per this question, but that doesn't seem to be called in this case.

Community
  • 1
  • 1
Connell
  • 13,925
  • 11
  • 59
  • 92
  • write custom filter attributes.. – techBeginner Jan 20 '14 at 17:19
  • @dotNETbeginner which type of filter attribute? I have a global `ExceptionFilterAttribute` set, but this doesn't seem to catch these errors, only exceptions thrown in my actions themselves. – Connell Jan 20 '14 at 17:24
  • Have a look at [this article](http://www.asp.net/mvc/tutorials/hands-on-labs/aspnet-mvc-4-custom-action-filters) – techBeginner Jan 21 '14 at 02:41

2 Answers2

1

It appears that these errors are added to the ModelState as model binding errors. The action selector selects the correct action and the action invoker invokes it without throwing any errors.

The workaround I came up with is to create an action invoker that checks the ModelState for errors. If it finds any, it passes the first one to the exception handling method used by my ExceptionFilter and ErrorController.

internal class ThrowModelStateErrorsActionInvoker : ApiControllerActionInvoker
{
    public override Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
    {
        foreach (var error in actionContext.ModelState.SelectMany(kvp => kvp.Value.Errors))
        {
            var exception = error.Exception ?? new ArgumentException(error.ErrorMessage);
            //invoke global exception handling
        }

        return base.InvokeActionAsync(actionContext, cancellationToken);
    }
}

It's nasty, but it works. This has taken up most of my day and I'm just glad to have finally got somewhere.

I'd be interested to know what the consequences are to this. What else uses the ModelState errors in Web API? Could anyone add some possible flaws in this solution?

Connell
  • 13,925
  • 11
  • 59
  • 92
0

It will more better if you use the new WebApi 2.1 Global error handling as discussed here, http://aspnetwebstack.codeplex.com/wikipage?title=Global%20Error%20Handling&referringTitle=Specs http://www.asp.net/web-api/overview/releases/whats-new-in-aspnet-web-api-21#global-error

If you are not willing to use WebApi 2.1 for a valid reason then you can try. (Note I have not tested but you can try). Create a custom action descriptor by inheriting with ReflectedHttpActionDescriptor and handle ExecuteAsync. This is what I mean,

public class HttpNotFoundActionDescriptor : ReflectedHttpActionDescriptor
{
    ReflectedHttpActionDescriptor _descriptor;
    public HttpNotFoundActionDescriptor(ReflectedHttpActionDescriptor descriptor)
    {
    _descriptor = descriptor;
    }

    public override Task<object> ExecuteAsync(HttpControllerContext controllerContext, IDictionary<string, object> arguments, CancellationToken cancellationToken)
    {
    try
        {
            return descriptor.ExecuteAsync(controllerContext, arguments, cancellationToken);
        }
        catch (HttpResponseException ex)
        {
         //..........................
    }   
    }
}

public class HttpNotFoundAwareControllerActionSelector : ApiControllerActionSelector
{
    public HttpNotFoundAwareControllerActionSelector()
    {
    }

    public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
    {
        HttpActionDescriptor decriptor = null;
        try
        {
            decriptor = base.SelectAction(controllerContext);
        }
        catch (HttpResponseException ex)
        {
            var code = ex.Response.StatusCode;
            if (code != HttpStatusCode.NotFound && code != HttpStatusCode.MethodNotAllowed)
                throw;
            var routeData = controllerContext.RouteData;
            routeData.Values["action"] = "Handle404";
            IHttpController httpController = new ErrorController();
            controllerContext.Controller = httpController;
            controllerContext.ControllerDescriptor = new HttpControllerDescriptor(controllerContext.Configuration, "Error", httpController.GetType());
            decriptor = base.SelectAction(controllerContext);
        }
        return new HttpNotFoundActionDescriptor(decriptor);
    }
}

Note you need to override all the virtual methods.

imran_ku07
  • 1,404
  • 12
  • 13
  • I've updated to MVC 5.1 and Web API 2.1, changed to an `IExceptionHandler` and whilst I do prefer this method to using filters, the routing error still bypasses this and displays the same error as before. – Connell Jan 21 '14 at 11:40
  • @ConnellWatkins The problem with using WebHost is that routing occurs before the Web API infrastructure is invoked, so there is nothing much Web API can do to help. There must be a way in the ASP.NET runtime to catch those errors though. – Darrel Miller Jan 21 '14 at 12:31
  • @ConnellWatkins, for route not found, you can use a custom match-everything route. See, http://stackoverflow.com/questions/619895/how-can-i-properly-handle-404-in-asp-net-mvc – imran_ku07 Jan 21 '14 at 12:52
  • @imran_ku07, thanks, but as stated in the question, I've already handled this. – Connell Jan 21 '14 at 12:54
  • @ConnellWatkins, you mean Global Error is not handling `The parameters dictionary contains a null entry for parameter`. – imran_ku07 Jan 21 '14 at 12:56
  • I handle invalid routes, invalid controller or action name. What I can't seem to handle is the action's parameters being incorrect types. The route, controller and action all exist, but the `{id}` in the route cannot be parsed as an integer. – Connell Jan 21 '14 at 13:00
  • Yes. The above code is the same method as the second link in my question. When `base.SelectAction(controllerContext);` is called, no exception is thrown as the action **does** exist. – Connell Jan 21 '14 at 13:04
  • @ConnellWatkins, I think you have misunderstood me. I am talking about HttpNotFoundActionDescriptor.ExecuteAsync, and you need to return HttpNotFoundActionDescriptor(decriptor) from HttpNotFoundAwareControllerActionSelector.SelectAction. See the code thoroughly. – imran_ku07 Jan 21 '14 at 13:07
  • Well the code itself didn't compile. I changed the descriptor's constructor's parameter to be a `HttpActionDescriptor`, but then I get a `NullReferenceException` in `InitializeFilterPipeline`. – Connell Jan 21 '14 at 13:18
  • I have the descriptor in place now using a different constructor (the parameterless one in the code above is only intended for unit testing according to the docs). However, same issue. `descriptor.ExecuteAsync(` does not throw an error. – Connell Jan 21 '14 at 13:34
  • it will be great if you share a sample repro? imran_ku07 at yahoo. Also please check the stack-trace – imran_ku07 Jan 21 '14 at 14:24
  • There is no error and therefore no stack trace. I have a working solution now, thanks for your help! I'll post as a separate answer. – Connell Jan 21 '14 at 15:30