Friday, November 28, 2014

Web API - Return Correct Status Codes for Exceptions

Returning the appropriate HTTP Response Codes back from your web server is a very important best practice. Fortunately for .NET developers, Web API makes it very easy to use Exception Filters to return the appropriate response codes from your exceptions.

By implementing a custom ExceptionFilterAttribute you can generically create and return HttpResponseMessages for unhandled exceptions based on type. This is great in that you do not have to wrap all of your controller actions in try catch blocks to handle exceptions from other application layers.

Sample Controller

public class ValuesController : ApiController
{
    public string Get(int id)
    {
        switch (id)
        {
            case 1:
                throw new KeyNotFoundException("Hello World");
 
            case 2:
                throw new ArgumentException("Goodnight Moon");
 
            default:
                return "value";
        }
    }
}

StatusCodeExceptionFilterAttribute

public class StatusCodeExceptionFilterAttribute : ExceptionFilterAttribute
{
    private static readonly Dictionary<Type, HttpStatusCode> TypeToCodeMap;
        
    static StatusCodeExceptionFilterAttribute()
    {
        TypeToCodeMap = new Dictionary<Type, HttpStatusCode>
        {
            {typeof (ArgumentException), HttpStatusCode.BadRequest},
            {typeof (SecurityException), HttpStatusCode.Forbidden},
            {typeof (WebException), HttpStatusCode.BadGateway},
            {typeof (FaultException), HttpStatusCode.BadGateway},
            {typeof (KeyNotFoundException), HttpStatusCode.NotFound},
            {typeof (DBConcurrencyException), HttpStatusCode.Conflict},
            {typeof (NotImplementedException), HttpStatusCode.NotImplemented}
        };
    }
 
    public override void OnException(HttpActionExecutedContext context)
    {
        var exception = GetUnderlyingException(context);
        var responseCode = GetStatusCode(exception);
        var httpError = CreateHttpError(context, exception);
 
        var response = context.Request.CreateErrorResponse(
            responseCode, 
            httpError);
 
        response.ReasonPhrase = exception.Message
            .Replace(Environment.NewLine, " ")
            .Replace("\r", " ")
            .Replace("\n", " ");
 
        context.Response = response;
    }
 
    private static HttpError CreateHttpError(
        HttpActionExecutedContext context, 
        Exception exception)
    {
        var includeErrorDetail = context.Request.ShouldIncludeErrorDetail();
        return new HttpError(exception, includeErrorDetail)
        {
            Message = exception.Message
        };
    }
 
    private static Exception GetUnderlyingException(
        HttpActionExecutedContext context)
    {
        if (context == null || context.Exception == null)
            return null;
 
        var aggregateException = context.Exception as AggregateException;
        var exception = aggregateException != null
            ? aggregateException.GetBaseException()
            : context.Exception;
 
        return exception;
    }
 
    private static HttpStatusCode GetStatusCode(Exception exception)
    {
        if (exception == null)
            return HttpStatusCode.OK;
 
        var exceptionType = exception.GetType();
 
        if (TypeToCodeMap.ContainsKey(exceptionType))
            return TypeToCodeMap[exceptionType];
 
        foreach (var pair in TypeToCodeMap)
            if (pair.Key.IsAssignableFrom(exceptionType))
                return pair.Value;
 
        return HttpStatusCode.InternalServerError;
    }
}

Sample Configuration

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services
        // Configure Web API to use only bearer token authentication.
        config.SuppressDefaultHostAuthentication();
 
        const string authType = OAuthDefaults.AuthenticationType;
        config.Filters.Add(new HostAuthenticationFilter(authType));
 
        config.Filters.Add(new StatusCodeExceptionFilterAttribute());
 
        // Web API routes
        config.MapHttpAttributeRoutes();
 
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

Enjoy,
Tom

No comments:

Post a Comment

Real Time Web Analytics