My beautiful REST webservice works great. Except if I visit pages like ~/
, which returns the default IIS 403 Forbidden page (even using Fiddler and specifying only Accept: application/json
). I want nothing but JSON or XML errors. Is there a way to override ALL exceptions with a custom exception handler? or a default controller to handle all unknown requests? What's the simplest, and the most correct (if different), way to handle this so that clients need only parse REST API-friendly XML datagrams or JSON blobs?
Example Request:
GET http://localhost:7414/ HTTP/1.1
User-Agent: Fiddler
Host: localhost:7414
Accept: application/json, text/json, text/xml
Response: (that I don't like, notice that text/html
wasn't one of the accepted response types)
HTTP/1.1 403 Forbidden
Cache-Control: private
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/8.0
X-SourceFiles: =?UTF-8?B?QzpcaWNhcm9sXENoYXJpdHlMb2dpYy5pQ2Fyb2wuQXBp?=
X-Powered-By: ASP.NET
Date: Fri, 25 Jan 2013 21:06:21 GMT
Content-Length: 5396
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>IIS 8.0 Detailed Error - 403.14 - Forbidden</title>
<style type="text/css">
<!--
...
Response (that I would prefer):
HTTP/1.1 403 Forbidden
Cache-Control: private
Content-Type: application/json; charset=utf-8
Date: ...
Content-Length: ....
{
"error":"forbidden",
"status":403,
"error_description":"Directory listing not allowed."
}
You can customize how Web API handles exceptions by writing an exception filter. An exception filter is executed when a controller method throws any unhandled exception that is not an HttpResponseException exception.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With