How to Handle Exceptions in ASP.NET Web API Like a Gentleman

Ah, the yellow screen of death.

YSOD

It’s a sure sign of an ASP.NET dev who has not taught their app to use its indoor voice. And there are many fine (and elsewhere documented) methods for dealing with a wayward exception before it turns your entire user interface into ketchup and mustard.

But I’m not here about that yellow screen of death.

HTTP 500 is the new YSOD

In a “modern” web app (as the kids these days call them), it’s much more likely that an uncaught something or other will poot forth from behind a REST API, presenting as a blob of JSON rather than as rendered markup.

Out of the box, ASP.NET Web API gift-wraps our code’s uncaught excremental output with an HTTP 500 response whose body contains more information than the consumer of the API probably needs:

HTTP 500 Internal Server Error

{
    "Message": "An error has occurred.",
    "ExceptionMessage": "No ticket.",
    "ExceptionType": "System.ArgumentNullException",
    "StackTrace": "   at IndianaJones.LastCrusade.Web.Api.TicketsPleaseController.Get() in TicketsPleaseController.cs:line 45\r\n   at lambda_method(Closure , Object , Object[] )\r\n   at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass13.<GetExecutor>b__c(Object instance, Object[] methodParameters)\r\n   at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Object instance, Object[] arguments)\r\n   at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.<>c__DisplayClass5.<ExecuteAsync>b__4()\r\n   at System.Threading.Tasks.TaskHelpers.RunSynchronously[TResult](Func`1 func, CancellationToken cancellationToken)"
}

This is the new Yellow Screen of Death. A RESTful sort of interface (the sort the Web API framework is built to deliver) should instead embrace and leverage its protocol, not work around or ignore it, as this default behavior does.

A more RESTafarian approach would be to return a slimmer response, perhaps composed of just the Exception’s Message property for the body, and (more importantly) bearing an HTTP Status Code that aligns with the nature of what’s actually gone awry. For instance, if bad user input was to blame, return a 400 (Bad Request). If a query for an specific record comes up empty, a 404 (Not Found) applies. And if somehow in the course of its operation your application suddenly became a whistling kettle of hot water, you could always send back a 418 (I’m a Teapot).

Web API makes doing this a piece of cake, by way of the HttpResponseException and its little friend, the HttpStatusCode enumeration:

public class TicketCollector {
 
    public void CollectFromPassenger(Ticket ticket) {
        if (ticket == null)
            throw new HttpResponseException(HttpStatusCode.BadRequest);
 
        // TODO: take ticket
    }
}

Now, make it right

As you can imagine, if we adopt this pattern across our codebase, our business logic will become shot through with references to System.Web.Http and HttpResponseException. Nasty.

I mean, business logic code is hands-down the most valuable part of any app, because it defines the features that are the application’s very reason for being. This code should be completely agnostic to from whence its input has come, and to how its output is ultimately being presented. If we want our app to be resilient and sustainable, we’ll have to unhitch this code from the System.Web.Http wagon.

We can accomplish this by updating our precious, precious logic to throw non-HTTP type exceptions. Then, in our Web API, we can catch these and translate them into HTTP exceptions as appropriate. Anything HTTP is solely the concern of the API code, and this division of labor keeps it that way.

public class TicketCollector {
 
    public void CollectFromPassenger(Ticket ticket) {
        if (ticket == null)
            throw new ArgumentNullException("ticket", "No ticket.");
 
        // TODO: take tickets, avoid Nazi eye contact
    }
}
 
// meanwhile
 
public class TicketsPleaseController : ApiController {
 
    public void Post(TicketRequest request) {
        try {
            var indy = new TicketCollector();
            indy.CollectFromPassenger(request.Ticket);
        }
        catch (ArgumentNullException) {
            throw new HttpResponseException(HttpStatusCode.BadRequest);
        }
        catch (Exception e) {
            throw new HttpResponseException(HttpStatusCode.NotAcceptable);
        }
    }
}

Concerns were separated, exceptions were handled, and lives were literally saved, you guys. But at this point, we’re only two-thirds of the way through the article–and that means we can do better.

Exception handling is not a feature

As our codebase evolves, this pattern will grow hair fast. The API’s pipes are getting clogged with prickly and distracting try/catch blocks. Even in our simple example above, there are eight lines of code dedicated to exception handling, compared to just two that relate to the actual meat of the call.

Exception handling is not a feature of our app, it is a cross-cutting aspect of it. Our code should reflect this by sweeping this aspect out of the way and returning focus to the feature. We can do this by rolling a custom ExceptionFilterAttribute:

public class HandleExceptionAttribute : ExceptionFilterAttribute {
 
    public Type Type { get; set; }
    public HttpStatusCode Status { get; set; }
 
    public override void OnException(HttpActionExecutedContext context) {
        var ex = context.Exception;
        if (ex.GetType() is Type) {
            var response = context.Request.CreateResponse<string>(Status, ex.Message);
            throw new HttpResponseException(response);
        }
    }
}

Now, just as our logic burns clean and knows nothing of HTTP, our API reads as just the plumbing we need it to be:

public class TicketsPleaseController : ApiController {
 
    [HandleException(Type = typeof(ArgumentNullException), Status = HttpStatusCode.BadRequest)]
    [HandleException(Type = typeof(Exception), Status = HttpStatusCode.NotAcceptable)]
    public void Post(TicketRequest request) {
        var indy = new TicketCollector();
        indy.CollectFromPassenger(request.Ticket);
    }
}

We can even stack several of them up in the order they should be handled, just as we do with multiple catch blocks.

The pool of available HTTP Status Codes may be limited, especially when compared to the limitless horizons of wrong your software may decide to throw. But with a little creativity, most levels of jacked-up can be conveyed just by using HTTP well. And with our HandleExceptionAttribute in the bag, there are no more excuses for rolling the HTTP 500 of Death.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s