I’ve been doing more work in the ASP.Net Web API Beta that’s part of MVC 4. One of the rather odd things I’ve come across is that despite the automatic content negotiation, the ASP.Net custom error page (yellow screen of death) still gets returned when an error occurs by default. If all you want to do is fix this, you can disable it and pass it along with the following setting in the system.webServer node in the web.config:

<httpErrors existingResponse="PassThrough" />

You still get the correct http status code, and now the error message will get serialized to a string and returned in the requested content type.

My desired scenario was slightly more complex, though. In my view, a proper API should have a consistent error format and some meaningful error messages. So I wanted to return a custom class I wrote with all of the details that I wanted returned, and have it serialized by content negotiation into the correct type. Something like the following:

[DataContract] public class MyCustomError {
[DataMember] public int Code { get; set; }
public string Headline { get; set; }
public string Message { get; set; }

public MyCustomError() { }
public MyCustomError(int ErrorCode, string ErrorTitle, string ErrorDetails) { Code = ErrorCode; Message = ErrorDetails; Headline = ErrorTitle; } }

Take note of the Serializable attribute and DataContract bits, they are important on there. So now that I have the class, I need to get it returned. Now you need another class that inherits from the ExceptionFilterAttribute. This will be what actually configures the error response back to the client when an error occurs.

    public class CustomResponseException : ExceptionFilterAttribute
        public override void OnException(HttpActionExecutedContext context)
            HttpStatusCode responseStatus = HttpStatusCode.InternalServerError;
            if (context.Exception.Data.Contains("StatusCode"))
                responseStatus = (HttpStatusCode)context.Exception.Data["StatusCode"];
            if (context.Exception.Data.Contains("MyCustomError"))
                context.Response = context.Request.CreateResponse<MyCustomError>(responseStatus, (MyCustomError)context.Exception.Data["MyCustomError"]);
                context.Response = context.Request.CreateResponse<MyCustomError>(responseStatus, new MyCustomError(ErrorNumber.UNSPECIFIEDERROR.ConvertToInt(), "Unknown error", "An error occurred."));
} base.OnException(context); } }

When I throw an error in code, I’m setting two pieces of data into the exception so that I can use it in this class. I put an instance of MyCustomError into the exception Data dictionary key “MyCustomError” so that I can return a specific message to the client. The second is a custom status code for cases like a 401 Unauthorized or a 404 NotFound, but much of the time I don’t set it as the 500 error is appropriate. Then I take these and put them in the context.Response, which along with the PassThrough attribute set in the web.config and the Serializable attribute on MyCustomError lets it automatically serialize to either XML or JSON according the client request type. (ErrorNumber in the code above is an enum that I’m using internally to signify the various custom error numbers for my API.) 

There’s one last step to actually get this hooked up. You have to either enable the CustomResponseException attribute globally, on a class level, or on a method level. If you want to do it class by class or method by method basis, use it as you would any attribute, by adding [CustomResponseException] above the method or class name. (Just like the [Serializable] on the MyCustomError class.) If you want it marked globally, go to the Application_Start method in Global.asax and add the line below.

GlobalConfiguration.Configuration.Filters.Add(new CustomResponseException());

Reference the links below for more information on the various parts of this.

Exception Filters

Another Custom Type Approach

httpErrors PassThrough setting

HttpStatusCode enum

Notes: This post was written during the Beta. I’ve updated the code for the release candidate on 2012-06-11.