Handling exceptions in your ASP.NET Web API

The Http status codes for reporting errors to clients can mainly be categorized on two groups, client errors and server errors. Any status code under 500 is considered an issue generated by something wrong on the request message sent by the client. For example, 404 for resource not found, 400 for bad request (some invalid data in the request message) or 403 for forbidden (an unauthorized operation) are some of the most well know client errors.  On the hand, any other code over 500 is considered as a problem on the server side such as 500 for internal server error or 503 for server unavailable. This kind of error means that something unexpected happened on the server side while processing the request but it is not the client fault.

It’s always a good practice when implementing a Web Api to use the correct http status codes for every situation. While it’s relatively easy to return a response with an specific status code in a controller action using the new HttpResponseMessage class, you might end up with a lot of repetitive code for handling all the possible exceptions in the different execution branches. All the dependencies for a controller like repositories or domain services are usually unaware of http details. For example, you might call a method in a repository that throws an exception when something is wrong, but it would be responsibility of the calling code in the Web Api controller to map that exception to an http status code.

As any cross cutting concern, exception handling can also be implemented in a centralized manner using a filter. This was the way it was implemented in MVC as well in the HandleErrorAttribute filter. ASP.NET Web API is not any different in that aspect, and you can also implement a custom filter for mapping an exception to an http status code.

This is how the filter implementation looks like,

public class ExceptionHandlerFilter : ExceptionFilterAttribute
{
    public ExceptionHandlerFilter()
    {
        this.Mappings = new Dictionary<Type, HttpStatusCode>();
        this.Mappings.Add(typeof(ArgumentNullException), HttpStatusCode.BadRequest);
        this.Mappings.Add(typeof(ArgumentException), HttpStatusCode.BadRequest);
    }
 
    public IDictionary<Type, HttpStatusCode> Mappings
    {
        get;
        private set;
    }
 
    public override void OnException(HttpActionExecutedContext actionExecutedContext)
    {
        if (actionExecutedContext.Exception != null)
        {
            var exception = actionExecutedContext.Exception;
 
            if (actionExecutedContext.Exception is HttpException)
            {
                var httpException = (HttpException)exception;
                actionExecutedContext.Result = new HttpResponseMessage<Error>(
                    new Error { Message = exception.Message },
                    (HttpStatusCode)httpException.GetHttpCode());
            }
            else if (this.Mappings.ContainsKey(exception.GetType()))
            {
                var httpStatusCode = this.Mappings[exception.GetType()];
                actionExecutedContext.Result = new HttpResponseMessage<Error>(
                    new Error { Message = exception.Message }, httpStatusCode);
            }
            else
            {
                actionExecutedContext.Result = new HttpResponseMessage<Error>(
                    new Error { Message = exception.Message }, HttpStatusCode.InternalServerError);
            }
        }
    }
}

As you can see, the filter derives from a built-in class ExceptionFilterAttribute that provides a virtual method “OnException” for implementing our exception handling code. The ExceptionFilterAttribute does not nothing by default.

The exception handling logic in this implementation mainly address three different scenarios,

  1. If an HttpException was raised anywhere in the Web API controller code, the status code and message in that exception will be reused and set in the response message.
  2. If the exception type is associated to an specific status code using a custom mapping, that status code will be used. For example, the filter automatically maps the ArgumentNullException to a “Bad Request” status code. The developer can customize this mapping when the filter is registered.
  3. Any other exception not found in the mapping is considered a server error and the generic “InternalServerError” status code is used.

In all the cases, a model for representing the exception is set in the response message so the framework will take care of serializing that using the wire format expected by the client. The HttpResponseMessage also contains a string property “ReasonPhrase”, which could be used to sent the exception message back to the client. However, this property does not seem to be sent correctly when everything is hosted in IIS.

Let see a few different cases of how this filter works in action.

public Contact Get(int id)
{
    var contact = repository.Get(id);
    if (contact == null)
        throw new HttpException((int)HttpStatusCode.NotFound, "Contact not found");
 
    //Do stuff
 
    return contact;
}

A contact was not found, so a new HttpException with status code 404 is thrown in the controller action. This will be automatically mapped to a response message with 404 in the exception filter.

public void Delete(int id)
{
    bool canBeDeleted = this.repository.CanDelete(id);
 
    if (!canBeDeleted)
    {
        throw new NotAuthorizedException("The contact can not be deleted");
    }
 
    this.repository.Delete(id);
}

Assuming the client did have permissions to delete an existing contact, a custom exception “NotAuthorizedException” was thrown in the controller action. The default behavior in the filter will be to set an internal server error (500) in the response message. However, that logic can be overriding by adding a new mapping for that exception when the filter is registered.

var exceptionHandler = new ExceptionHandlerFilter();
 
exceptionHandler.Mappings.Add(typeof(NotAuthorizedException), HttpStatusCode.Forbidden);
 
GlobalConfiguration.Configuration.Filters.Add(exceptionHandler);

In that way, the client will receive a more meaningful status code representing a forbidden action.

Comments

No Comments