Error Handling in ASP.NET WebAPI

Error Handling in ASP.NET WebAPI

  • Comments 6

WebAPI is a brand new framework that makes it easy to build HTTP services. As such, it provides several features that make it easy to send back useful and informative error messages in a variety of cases. In this post, I’ll go over some of these capabilities.

Note: Not all of the features I mention below are part of the ASP.NET MVC 4 RC build. You may have to use our nightly Nuget packages to get access to our latest bits. You can find more information about how to do so here: http://aspnetwebstack.codeplex.com/discussions/353867.

Now that’s out of the way, let’s take a look at what a typical WebAPI error’s HTTP content looks like:

{
  "Message": "No HTTP resource was found that matches the request URI 'http://localhost/Foo'.",
  "MessageDetail": "No type was found that matches the controller named 'Foo'."
}

So the error is actually just a collection of key-value pairs that provide information about what went wrong. This collection of key-value pairs is then sent back to the client in whatever content type it asked for through HTTP content negotiation. In the example above, the content is actually JSON.

But if you had “text/xml” in your request’s Accept header for example, you might get this response instead:

<Error>
  <Message>No HTTP resource was found that matches the request URI '
http://local
host/Foo'.</Message>
  <MessageDetail>No type was found that matches the controller named 'Foo'.</MessageDetail>

< /Error>

This simple format makes it easy for clients to understand what went wrong or to extract relevant error information for error logging or reporting.

HttpError

What you just saw above is actually an instance of an HttpError being serialized to the wire by Json.NET and DataContractSerializer respectively.

The HttpError type defines a consistent and extensible way for representing error information across the framework. Effectively, it’s just a Dictionary<string, object> that provides some helpers for creating errors that contain error messages, exceptions, or invalid model states. HttpError defines the following public constructors:

   1: public HttpError();
   2: public HttpError(string message);
   3: public HttpError(Exception exception, bool includeErrorDetail);
   4: public HttpError(ModelStateDictionary modelState, bool includeErrorDetail);

Here’s what HttpErrors for Exceptions look like in JSON:

{
  "Message": "An error has occurred.",
  "ExceptionMessage": "Index was outside the bounds of the array.",
  "ExceptionType": "System.IndexOutOfRangeException",
  "StackTrace": "   at WebApiTest.TestController.Post(Uri uri) in c:\\Temp\\WebApiTest\\WebApiTest\\TestController.cs:line 18\r\n   at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClassf.<GetExecutor>b__9(Object instance, Object[] methodParameters)\r\n   at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Object instance, Object[] arguments)\r\n   at System.Threading.Tasks.TaskHelpers.RunSynchronously[TResult](Func`1 func, CancellationToken cancellationToken)"
}

And for invalid ModelStates:

{
  "Message": "The request is invalid.",
  "ModelState": {
    "s": [
      "Required property 's' not found in JSON. Path '', line 1, position 2."
    ]
  }
}

But it’s also important to note that you can create your own custom HttpErrors with your own custom error information. Here’s an example:

   1: public HttpResponseMessage Get()
   2: {
   3:     HttpError myCustomError = new HttpError("My custom error message") { { "CustomErrorCode", 37 } };
   4:     return Request.CreateErrorResponse(HttpStatusCode.BadRequest, myCustomError);
   5: }

This results in the following response:

{
  "Message": "My custom error message",
  "CustomErrorCode": 37
}

This is the first example where you’ve actually seen an action implemented that returns an error. Notice that we’re using Request.CreateErrorResponse to create an HttpResponseMessage that wraps our custom error. This is the recommended way to create response messages for errors. The following extension methods are provided to make it easy to return errors from your own code as well:

   1: public static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, Exception exception);
   2: public static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, HttpError error);
   3: public static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, ModelStateDictionary modelState);
   4: public static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, string message);
   5: public static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, string message, Exception exception);

You can also choose to just throw any exception from your action. WebAPI will automatically catch the exception and convert it into an error response with status code 500 (Internal Server Error) in exactly the same format as an error response that you would have created for an exception in your own code.

HttpResponseException

Now you may be wondering: how do I return an error if my action returns a different type instead of HttpResponseMessage? This is where HttpResponseException comes in. HttpResponseException is an exception type defined for WebAPI that serves two purposes:

  1. It allows you to return a specific HTTP response from actions with return types that aren’t HttpResponseMessage.
  2. It allows you to short-circuit the WebAPI pipeline pretty much anywhere and return a response immediately.

Technically, HttpResponseException can be used for any kind of HTTP response, but it’s especially useful in error cases. It allows us to implement actions like this:

   1: public Person Get(int id)
   2: {
   3:     if (!_contacts.ContainsKey(id))
   4:     {
   5:         throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.NotFound, String.Format("Contact {0} not found.", id)));
   6:     }
   7:  
   8:     return _contacts[id];
   9: }

Notice how even though the action is declared to return a Person, we’re able to return a custom error back to the client in error cases.

Error Detail

You may have noticed the “includeErrorDetail” parameter earlier in this post on the HttpError constructor. This is because typically, servers want to send different error information depending on whether the client is just consuming the service or whether the client needs additional debugging information to understand what went wrong on the server. By default, WebAPI will not send error details to remote clients and will provide these additional error details to clients on the local machine. Returning to our first example, whereas a local client would see this:

{
  "Message": "No HTTP resource was found that matches the request URI 'http://localhost/Foo'.",
  "MessageDetail": "No type was found that matches the controller named 'Foo'."
}

A remote client would only see this:

{
  "Message": "No HTTP resource was found that matches the request URI 'http://localhost/Foo'."
}

The difference between Message and MessageDetail above is that MessageDetail contains error information that is WebAPI-specific that remote clients should not have to see in most cases. The exact meaning of error detail depends on the case. For exceptions, error detail includes the exception message, exception type, the stack trace, and the inner exceptions. Only a vague “An error has occurred.” message is sent back to remote clients for exceptions by default. For model states, any model error messages are sent to remote clients. Model error exceptions, however, are considered detail and will not get sent to remote clients by default.

You can choose to override when the error detail gets sent back to clients directly on your HttpConfiguration object:

   1: config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.LocalOnly;
   2: config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
   3: config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Never;

As mentioned above, the default is LocalOnly.

Applying Error Handling to Handle Invalid ModelStates

One especially common use of WebAPI’s error handling is to immediately return an error if the model state is invalid. This can be done fairly easily by implementing the following action filter:

   1: public class ValidationFilterAttribute : ActionFilterAttribute
   2: {
   3:     public override void OnActionExecuting(HttpActionContext actionContext)
   4:     {
   5:         if (!actionContext.ModelState.IsValid)
   6:         {
   7:             actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
   8:         }
   9:     }
  10: }

You can then either mark your action with this attribute, or register the action filer globally like this:

   1: config.Filters.Add(new ValidationFilterAttribute());

This allows you to short-circuit the processing of the request and immediately return an error for the invalid model state back to the client.

As always, you can learn more about ASP.NET WebAPI here: http://www.asp.net/web-api.

Leave a Comment
  • Please add 2 and 8 and type the answer here:
  • Post
  • Trying to follow your approach, can't find class HttpError. Where is it defined?

  • It's defined right here:

    aspnetwebstack.codeplex.com/.../944ab06b9c54

    in the System.Web.Http dll and namespace. But as I mentioned earlier, you'll need post-RC bits to be able to use it.

  • may i use webapi under vs2008 with .net3.5?

  • You'll need at least .NET 4.0 to get WebAPI running.

  • You can disregard my last comment.  My custom action filter was originally an ActionFilter and not an ExceptionActionFilter.  When I changed it to be an ExceptionActionFilter, then both the ELMAH filter and mine got hit correctly.

  • do you need a special dll to get the ExceptionActionFilterAttribute ?

    Doesn't show up under a standard web api template setup...

Page 1 of 1 (6 items)