Edit 1/26/14: Microsoft just added "Global Error Handling" to the latest WebAPI 2.1 update.
Ok, I think I've got it. There's a few parts to it.
First: Create a controller for your errors. I named my actions according to the HTTP error codes.
public class ErrorController : ApiController {
[AllowAnonymous]
[ActionName("Get")]
public HttpResponseMessage Get() {
return Request.CreateErrorInfoResponse(HttpStatusCode.InternalServerError, title: "Unknown Error");
}
[AllowAnonymous]
[ActionName("404")]
[HttpGet]
public HttpResponseMessage Status404() {
return Request.CreateErrorInfoResponse(HttpStatusCode.NotFound, description: "No resource matches the URL specified.");
}
[AllowAnonymous]
[ActionName("400")]
[HttpGet]
public HttpResponseMessage Status400() {
return Request.CreateErrorInfoResponse(HttpStatusCode.BadRequest);
}
[AllowAnonymous]
[ActionName("500")]
[HttpGet]
public HttpResponseMessage Status500() {
return Request.CreateErrorInfoResponse(HttpStatusCode.InternalServerError);
}
}
Next, I created a GenericExceptionFilterAttribute
that checks to see if the HttpActionExecutedContext.Exception is populated and if the response is still empty. If both cases are true, then it generates a response.
public class GenericExceptionFilterAttribute : ExceptionFilterAttribute {
public GenericExceptionFilterAttribute()
: base() {
DefaultHandler = (context, ex) => context.Request.CreateErrorInfoResponse(System.Net.HttpStatusCode.InternalServerError, "Internal Server Error", "An unepected error occoured on the server.", exception: ex);
}
readonly Dictionary<Type, Func<HttpActionExecutedContext, Exception, HttpResponseMessage>> exceptionHandlers = new Dictionary<Type, Func<HttpActionExecutedContext, Exception, HttpResponseMessage>>();
public Func<HttpActionExecutedContext, Exception, HttpResponseMessage> DefaultHandler { get; set; }
public void AddExceptionHandler<T>(Func<HttpActionExecutedContext, Exception, HttpResponseMessage> handler) where T : Exception {
exceptionHandlers.Add(typeof(T), handler);
}
public override void OnException(HttpActionExecutedContext context) {
if (context.Exception == null) return;
try {
var exType = context.Exception.GetType();
if (exceptionHandlers.ContainsKey(exType))
context.Response = exceptionHandlers[exType](context, context.Exception);
if(context.Response == null && DefaultHandler != null)
context.Response = DefaultHandler(context, context.Exception);
}
catch (Exception ex) {
context.Response = context.Request.CreateErrorInfoResponse(HttpStatusCode.InternalServerError, description: "Error while building the exception response.", exception: ex);
}
}
}
In my case, I went with a single generic handler that I could register support for each of the main exception types and map each of those exception types to specific HTTP response codes. Now register your exception types and handlers this filter globally in your global.asax.cs
:
// These filters override the default ASP.NET exception handling to create REST-Friendly error responses.
var exceptionFormatter = new GenericExceptionFilterAttribute();
exceptionFormatter.AddExceptionHandler<NotImplementedException>((context, ex) => context.Request.CreateErrorInfoResponse(System.Net.HttpStatusCode.InternalServerError, "Not Implemented", "This method has not yet been implemented. Please try your request again at a later date.", exception: ex));
exceptionFormatter.AddExceptionHandler<ArgumentException>((context, ex) => context.Request.CreateErrorInfoResponse(System.Net.HttpStatusCode.BadRequest, exception: ex));
exceptionFormatter.AddExceptionHandler<ArgumentNullException>((context, ex) => context.Request.CreateErrorInfoResponse(System.Net.HttpStatusCode.BadRequest, exception: ex));
exceptionFormatter.AddExceptionHandler<ArgumentOutOfRangeException>((context, ex) => context.Request.CreateErrorInfoResponse(System.Net.HttpStatusCode.BadRequest, exception: ex));
exceptionFormatter.AddExceptionHandler<FormatException>((context, ex) => context.Request.CreateErrorInfoResponse(System.Net.HttpStatusCode.BadRequest, exception: ex));
exceptionFormatter.AddExceptionHandler<NotSupportedException>((context, ex) => context.Request.CreateErrorInfoResponse(System.Net.HttpStatusCode.BadRequest, "Not Supported", exception: ex));
exceptionFormatter.AddExceptionHandler<InvalidOperationException>((context, ex) => context.Request.CreateErrorInfoResponse(System.Net.HttpStatusCode.BadRequest, "Invalid Operation", exception: ex));
GlobalConfiguration.Filters.Add(exceptionFormatter)
Next, create a catchall route to send all unknown requests to your new Error handler:
config.Routes.MapHttpRoute(
name: "DefaultCatchall",
routeTemplate: "{*url}",
defaults: new {
controller = "Error",
action = "404"
}
);
And, to wrap it all up, let IIS process all requests through ASP.NET by adding this to your web.config
:
<configuration>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true" />
</system.webServer>
</configuration>
Optionally, you could also use the customErrors
section of the web.config
to redirect all errors to your new error handler.