181
votes

After reviewing an article Exception Handling in ASP.NET Web API I am a bit confused as to when to throw an exception vs return an error response. I am also left wondering whether it is possible to modify the response when your method returns a domain specific model instead of HttpResponseMessage...

So, to recap here are my questions followed by some code with case #s:

Questions

Questions regarding Case #1

  1. Should I always use HttpResponseMessage instead of a concrete domain model, so that the message can be customized?
  2. Can the message be customized if you are returning concrete domain model?

Questions regarding Case #2,3,4

  1. Should I be throwing an exception or returning error response? If the answer is "it depends", can you give situations/examples on when to use one vs the other.
  2. What is the difference between throwing HttpResponseException vs Request.CreateErrorResponse ? The output to client seems identical...
  3. Should I always use HttpError to "wrap" response messages in errors (whether the exception is thrown or error response returned)?

Case Samples

// CASE #1
public Customer Get(string id)
{
    var customer = _customerService.GetById(id);
    if (customer == null)
    {
        var notFoundResponse = new HttpResponseMessage(HttpStatusCode.NotFound);
        throw new HttpResponseException(notFoundResponse);
    }
    //var response = Request.CreateResponse(HttpStatusCode.OK, customer);
    //response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
    return customer;
}        

// CASE #2
public HttpResponseMessage Get(string id)
{
    var customer = _customerService.GetById(id);
    if (customer == null)
    {
        var notFoundResponse = new HttpResponseMessage(HttpStatusCode.NotFound);
        throw new HttpResponseException(notFoundResponse);
    }
    var response = Request.CreateResponse(HttpStatusCode.OK, customer);
    response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
    return response;
}

// CASE #3
public HttpResponseMessage Get(string id)
{
    var customer = _customerService.GetById(id);
    if (customer == null)
    {
        var message = String.Format("customer with id: {0} was not found", id);
        var errorResponse = Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
        throw new HttpResponseException(errorResponse);
    }
    var response = Request.CreateResponse(HttpStatusCode.OK, customer);
    response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
    return response;
}

// CASE #4
public HttpResponseMessage Get(string id)
{
    var customer = _customerService.GetById(id);
    if (customer == null)
    {
        var message = String.Format("customer with id: {0} was not found", id);
        var httpError = new HttpError(message);
        return Request.CreateErrorResponse(HttpStatusCode.NotFound, httpError);
    }
    var response = Request.CreateResponse(HttpStatusCode.OK, customer);
    response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
    return response;
}

Update

To help further demonstrate cases #2,3,4 the following code snippet highlights several options that "can happen" when a customer is not found...

if (customer == null)
{
    // which of these 4 options is the best strategy for Web API?

    // option 1 (throw)
    var notFoundMessage = new HttpResponseMessage(HttpStatusCode.NotFound);
    throw new HttpResponseException(notFoundMessage);

    // option 2 (throw w/ HttpError)
    var message = String.Format("Customer with id: {0} was not found", id);
    var httpError = new HttpError(message);
    var errorResponse = Request.CreateErrorResponse(HttpStatusCode.NotFound, httpError);
    throw new HttpResponseException(errorResponse);

    // option 3 (return)
    var message = String.Format("Customer with id: {0} was not found", id);
    return Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
    // option 4 (return w/ HttpError)
    var message = String.Format("Customer with id: {0} was not found", id);
    var httpError = new HttpError(message);
    return Request.CreateErrorResponse(HttpStatusCode.NotFound, httpError);
}
9
@Mike Wasson As the author of the linked article, which approach would you take?zam6ak

9 Answers

108
votes

The approach I have taken is to just throw exceptions from the api controller actions and have an exception filter registered that processes the exception and sets an appropriate response on the action execution context.

The filter exposes a fluent interface that provides a means of registering handlers for specific types of exceptions prior to registering the filter with global configuration.

The use of this filter enables centralized exception handling instead of spreading it across the controller actions. There are however cases where I will catch exceptions within the controller action and return a specific response if it does not make sense to centralize the handling of that particular exception.

Example registration of filter:

GlobalConfiguration.Configuration.Filters.Add(
    new UnhandledExceptionFilterAttribute()
    .Register<KeyNotFoundException>(HttpStatusCode.NotFound)

    .Register<SecurityException>(HttpStatusCode.Forbidden)

    .Register<SqlException>(
        (exception, request) =>
        {
            var sqlException = exception as SqlException;

            if (sqlException.Number > 50000)
            {
                var response            = request.CreateResponse(HttpStatusCode.BadRequest);
                response.ReasonPhrase   = sqlException.Message.Replace(Environment.NewLine, String.Empty);

                return response;
            }
            else
            {
                return request.CreateResponse(HttpStatusCode.InternalServerError);
            }
        }
    )
);

UnhandledExceptionFilterAttribute class:

using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Web.Http.Filters;

namespace Sample
{
    /// <summary>
    /// Represents the an attribute that provides a filter for unhandled exceptions.
    /// </summary>
    public class UnhandledExceptionFilterAttribute : ExceptionFilterAttribute
    {
        #region UnhandledExceptionFilterAttribute()
        /// <summary>
        /// Initializes a new instance of the <see cref="UnhandledExceptionFilterAttribute"/> class.
        /// </summary>
        public UnhandledExceptionFilterAttribute() : base()
        {

        }
        #endregion

        #region DefaultHandler
        /// <summary>
        /// Gets a delegate method that returns an <see cref="HttpResponseMessage"/> 
        /// that describes the supplied exception.
        /// </summary>
        /// <value>
        /// A <see cref="Func{Exception, HttpRequestMessage, HttpResponseMessage}"/> delegate method that returns 
        /// an <see cref="HttpResponseMessage"/> that describes the supplied exception.
        /// </value>
        private static Func<Exception, HttpRequestMessage, HttpResponseMessage> DefaultHandler = (exception, request) =>
        {
            if(exception == null)
            {
                return null;
            }

            var response            = request.CreateResponse<string>(
                HttpStatusCode.InternalServerError, GetContentOf(exception)
            );
            response.ReasonPhrase   = exception.Message.Replace(Environment.NewLine, String.Empty);

            return response;
        };
        #endregion

        #region GetContentOf
        /// <summary>
        /// Gets a delegate method that extracts information from the specified exception.
        /// </summary>
        /// <value>
        /// A <see cref="Func{Exception, String}"/> delegate method that extracts information 
        /// from the specified exception.
        /// </value>
        private static Func<Exception, string> GetContentOf = (exception) =>
        {
            if (exception == null)
            {
                return String.Empty;
            }

            var result  = new StringBuilder();

            result.AppendLine(exception.Message);
            result.AppendLine();

            Exception innerException = exception.InnerException;
            while (innerException != null)
            {
                result.AppendLine(innerException.Message);
                result.AppendLine();
                innerException = innerException.InnerException;
            }

            #if DEBUG
            result.AppendLine(exception.StackTrace);
            #endif

            return result.ToString();
        };
        #endregion

        #region Handlers
        /// <summary>
        /// Gets the exception handlers registered with this filter.
        /// </summary>
        /// <value>
        /// A <see cref="ConcurrentDictionary{Type, Tuple}"/> collection that contains 
        /// the exception handlers registered with this filter.
        /// </value>
        protected ConcurrentDictionary<Type, Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>> Handlers
        {
            get
            {
                return _filterHandlers;
            }
        }
        private readonly ConcurrentDictionary<Type, Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>> _filterHandlers = new ConcurrentDictionary<Type, Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>>();
        #endregion

        #region OnException(HttpActionExecutedContext actionExecutedContext)
        /// <summary>
        /// Raises the exception event.
        /// </summary>
        /// <param name="actionExecutedContext">The context for the action.</param>
        public override void OnException(HttpActionExecutedContext actionExecutedContext)
        {
            if(actionExecutedContext == null || actionExecutedContext.Exception == null)
            {
                return;
            }

            var type    = actionExecutedContext.Exception.GetType();

            Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> registration = null;

            if (this.Handlers.TryGetValue(type, out registration))
            {
                var statusCode  = registration.Item1;
                var handler     = registration.Item2;

                var response    = handler(
                    actionExecutedContext.Exception.GetBaseException(), 
                    actionExecutedContext.Request
                );

                // Use registered status code if available
                if (statusCode.HasValue)
                {
                    response.StatusCode = statusCode.Value;
                }

                actionExecutedContext.Response  = response;
            }
            else
            {
                // If no exception handler registered for the exception type, fallback to default handler
                actionExecutedContext.Response  = DefaultHandler(
                    actionExecutedContext.Exception.GetBaseException(), actionExecutedContext.Request
                );
            }
        }
        #endregion

        #region Register<TException>(HttpStatusCode statusCode)
        /// <summary>
        /// Registers an exception handler that returns the specified status code for exceptions of type <typeparamref name="TException"/>.
        /// </summary>
        /// <typeparam name="TException">The type of exception to register a handler for.</typeparam>
        /// <param name="statusCode">The HTTP status code to return for exceptions of type <typeparamref name="TException"/>.</param>
        /// <returns>
        /// This <see cref="UnhandledExceptionFilterAttribute"/> after the exception handler has been added.
        /// </returns>
        public UnhandledExceptionFilterAttribute Register<TException>(HttpStatusCode statusCode) 
            where TException : Exception
        {

            var type    = typeof(TException);
            var item    = new Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>(
                statusCode, DefaultHandler
            );

            if (!this.Handlers.TryAdd(type, item))
            {
                Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> oldItem = null;

                if (this.Handlers.TryRemove(type, out oldItem))
                {
                    this.Handlers.TryAdd(type, item);
                }
            }

            return this;
        }
        #endregion

        #region Register<TException>(Func<Exception, HttpRequestMessage, HttpResponseMessage> handler)
        /// <summary>
        /// Registers the specified exception <paramref name="handler"/> for exceptions of type <typeparamref name="TException"/>.
        /// </summary>
        /// <typeparam name="TException">The type of exception to register the <paramref name="handler"/> for.</typeparam>
        /// <param name="handler">The exception handler responsible for exceptions of type <typeparamref name="TException"/>.</param>
        /// <returns>
        /// This <see cref="UnhandledExceptionFilterAttribute"/> after the exception <paramref name="handler"/> 
        /// has been added.
        /// </returns>
        /// <exception cref="ArgumentNullException">The <paramref name="handler"/> is <see langword="null"/>.</exception>
        public UnhandledExceptionFilterAttribute Register<TException>(Func<Exception, HttpRequestMessage, HttpResponseMessage> handler) 
            where TException : Exception
        {
            if(handler == null)
            {
              throw new ArgumentNullException("handler");
            }

            var type    = typeof(TException);
            var item    = new Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>(
                null, handler
            );

            if (!this.Handlers.TryAdd(type, item))
            {
                Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> oldItem = null;

                if (this.Handlers.TryRemove(type, out oldItem))
                {
                    this.Handlers.TryAdd(type, item);
                }
            }

            return this;
        }
        #endregion

        #region Unregister<TException>()
        /// <summary>
        /// Unregisters the exception handler for exceptions of type <typeparamref name="TException"/>.
        /// </summary>
        /// <typeparam name="TException">The type of exception to unregister handlers for.</typeparam>
        /// <returns>
        /// This <see cref="UnhandledExceptionFilterAttribute"/> after the exception handler 
        /// for exceptions of type <typeparamref name="TException"/> has been removed.
        /// </returns>
        public UnhandledExceptionFilterAttribute Unregister<TException>()
            where TException : Exception
        {
            Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> item = null;

            this.Handlers.TryRemove(typeof(TException), out item);

            return this;
        }
        #endregion
    }
}

Source code can also be found here.

26
votes

If you are not returning HttpResponseMessage and instead are returning entity/model classes directly, an approach which I have found useful is to add the following utility function to my controller

private void ThrowResponseException(HttpStatusCode statusCode, string message)
{
    var errorResponse = Request.CreateErrorResponse(statusCode, message);
    throw new HttpResponseException(errorResponse);
}

and simply call it with the appropriate status code and message

18
votes

Do not throw an HttpResponseException or return an HttpResponesMessage for errors - except if the intent is to end the request with that exact result.

HttpResponseException's are not handled the same as other exceptions. They are not caught in Exception Filters. They are not caught in the Exception Handler. They are a sly way to slip in an HttpResponseMessage while terminating the current code's execution flow.

Unless the code is infrastructure code relying on this special un-handling, avoid using the HttpResponseException type!

HttpResponseMessage's are not exceptions. They do not terminate the current code's execution flow. They can not be filtered as exceptions. They can not be logged as exceptions. They represent a valid result - even a 500 response is "a valid non-exception response"!


Make life simpler:

When there is an exceptional/error case, go ahead and throw a normal .NET exception - or a customized application exception type (not deriving from HttpResponseException) with desired 'http error/response' properties such as a status code - as per normal exception handling.

Use Exception Filters / Exception Handlers / Exception Loggers to do something appropriate with these exceptional cases: change/add status codes? add tracking identifiers? include stack traces? log?

By avoiding HttpResponseException the 'exceptional case' handling is made uniform and can be handled as part of the exposed pipeline! For example one can turn a 'NotFound' into a 404 and an 'ArgumentException' into a 400 and a 'NullReference' into a 500 easily and uniformly with application-level exceptions - while allowing extensibility to provide "basics" such as error logging.

15
votes

Case #1

  1. Not necessarily, there are other places in the pipeline to modify the response (action filters, message handlers).
  2. See above -- but if the action returns a domain model, then you can't modify the response inside the action.

Cases #2-4

  1. The main reasons to throw HttpResponseException are:
    • if you are returning a domain model but need to handle error cases,
    • to simplify your controller logic by treating errors as exceptions
  2. These should be equivalent; HttpResponseException encapsulates an HttpResponseMessage, which is what gets returned back as the HTTP response.

    e.g., case #2 could be rewritten as

    public HttpResponseMessage Get(string id)
    {
        HttpResponseMessage response;
        var customer = _customerService.GetById(id);
        if (customer == null)
        {
            response = new HttpResponseMessage(HttpStatusCode.NotFound);
        }
        else
        {
            response = Request.CreateResponse(HttpStatusCode.OK, customer);
            response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
        }
        return response;
    }
    

    ... but if your controller logic is more complicated, throwing an exception might simplify the code flow.

  3. HttpError gives you a consistent format for the response body and can be serialized to JSON/XML/etc, but it's not required. e.g., you may not want to include an entity-body in the response, or you might want some other format.

9
votes

Another case for when to use HttpResponseException instead of Response.CreateResponse(HttpStatusCode.NotFound), or other error status code, is if you have transactions in action filters and you want the transactions to be rolled back when returning an error response to the client.

Using Response.CreateResponse will not roll the transaction back, whereas throwing an exception will.

3
votes

I want to point out that it has been my experience that if throwing an HttpResponseException instead of returning an HttpResponseMessage in a webapi 2 method, that if a call is made immediately to IIS Express it will timeout or return a 200 but with a html error in the response. Easiest way to test this is to make $.ajax call to a method that throws a HttpResponseException and in the errorCallBack in ajax make an immediate call to another method or even a simple http page. You will notice the imediate call will fail. If you add a break point or a settimeout() in the error call back to delay the second call a second or two giving the server time to recover it works correctly. This makes no since but its almost like the throw HttpResponseException causes the server side listener thread to exit and restart causing a split second of no server accepting connections or something.

Update: The root cause of the wierd Ajax connection Timeout is if an ajax call is made quick enough the same tcp connection is used. I was raising a 401 error ether by returning a HttpResonseMessage or throwing a HTTPResponseException which was returned to the browser ajax call. But along with that call MS was returning a Object Not Found Error because in Startup.Auth.vb app.UserCookieAuthentication was enabled so it was trying to return intercept the response and add a redirect but it errored with Object not Instance of an Object. This Error was html but was appended to the response after the fact so only if the ajax call was made quick enough and the same tcp connection used did it get returned to the browser and then it got appended to the front of the next call. For some reason Chrome just timedout, fiddler pucked becasue of the mix of json and htm but firefox rturned the real error. So wierd but packet sniffer or firefox was the only way to track this one down.

Also it should be noted that if you are using Web API help to generate automatic help and you return HttpResponseMessage then you should add a

[System.Web.Http.Description.ResponseType(typeof(CustomReturnedType))] 

attribute to the method so the help generates correctly. Then

return Request.CreateResponse<CustomReturnedType>(objCustomeReturnedType) 

or on error

return Request.CreateErrorResponse( System.Net.HttpStatusCode.InternalServerError, new Exception("An Error Ocurred"));

Hope this helps someone else who may be getting random timeout or server not available immediately after throwing a HttpResponseException.

Also returning an HttpResponseException has the added benefit of not causing Visual Studio to break on an Un-handled exception usefull when the error being returned is the AuthToken needs to be refreshed in a single page app.

Update: I am retracting my statement about IIS Express timing out, this happened to be a mistake in my client side ajax call back it turns out since Ajax 1.8 returning $.ajax() and returning $.ajax.().then() both return promise but not the same chained promise then() returns a new promise which caused the order of execution to be wrong. So when the then() promise completed it was a script timeout. Weird gotcha but not an IIS express issue a problem between the Keyboard and chair.

0
votes

As far as I can tell, whether you throw an exception, or you return Request.CreateErrorResponse, the result is the same. If you look at the source code for System.Web.Http.dll, you will see as much. Take a look at this general summary, and a very similar solution that I have made: Web Api, HttpError, and the behavior of exceptions

0
votes

In error situations, I wanted to return a specific error details class, in whatever format the client requested instead of the happy path object.

I want to have my controller methods return the domain specific happy path object and to throw an exception otherwise.

The problem I had was that the HttpResponseException constructors do not allow domain objects.

This is what I eventually came up with

public ProviderCollection GetProviders(string providerName)
{
   try
   {
      return _providerPresenter.GetProviders(providerName);
   }
   catch (BadInputValidationException badInputValidationException)
   {
     throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest,
                                          badInputValidationException.Result));
   }
}

Result is a class that contains error details, while ProviderCollection is my happy path result.

0
votes

I like Oppositional answer

Anyway, I needed a way to catch the inherited Exception and that solution doesn't satisfy all my needs.

So I ended up changing how he handles OnException and this is my version

public override void OnException(HttpActionExecutedContext actionExecutedContext) {
   if (actionExecutedContext == null || actionExecutedContext.Exception == null) {
      return;
   }

   var type = actionExecutedContext.Exception.GetType();

   Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> registration = null;

   if (!this.Handlers.TryGetValue(type, out registration)) {
      //tento di vedere se ho registrato qualche eccezione che eredita dal tipo di eccezione sollevata (in ordine di registrazione)
      foreach (var item in this.Handlers.Keys) {
         if (type.IsSubclassOf(item)) {
            registration = this.Handlers[item];
            break;
         }
      }
   }

   //se ho trovato un tipo compatibile, uso la sua gestione
   if (registration != null) {
      var statusCode = registration.Item1;
      var handler = registration.Item2;

      var response = handler(
         actionExecutedContext.Exception.GetBaseException(),
         actionExecutedContext.Request
      );

      // Use registered status code if available
      if (statusCode.HasValue) {
         response.StatusCode = statusCode.Value;
      }

      actionExecutedContext.Response = response;
   }
   else {
      // If no exception handler registered for the exception type, fallback to default handler
      actionExecutedContext.Response = DefaultHandler(actionExecutedContext.Exception.GetBaseException(), actionExecutedContext.Request
      );
   }
}

The core is this loop where I check if the exception type is a subclass of a registered type.

foreach (var item in this.Handlers.Keys) {
    if (type.IsSubclassOf(item)) {
        registration = this.Handlers[item];
        break;
    }
}

my2cents