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:
HttpResponseMessage
instead of a concrete domain model, so that the message can be customized?HttpResponseException
vs Request.CreateErrorResponse
? The output to client seems identical...HttpError
to "wrap" response messages in errors (whether the exception is thrown or error response returned)?// 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; }
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); }
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.
Exceptions are the errors that happen at runtime. Exception handling is the technique to handle this runtime error in our application code. If any error is thrown in web API that is caught, it is translated into an HTTP response with status code 500- "Internal Server Error".
Use a try block around the statements that might throw exceptions. Once an exception occurs in the try block, the flow of control jumps to the first associated exception handler that is present anywhere in the call stack. In C#, the catch keyword is used to define an exception handler.
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.
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
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