Technical Blogs

12/29/2014
WebAPI Exception Reporting

December 29th, 2014

WebAPI Exception Reporting

By default, WebAPI does not report exceptions very well. Developers will recieve a "yellow" server error page rather than a code handleable exception. These Html resposes gum up the works when the system is attempting to deserialize json. This blog entry contains one approach to ensuring exceptions are passed back to the client for processing.

The first things that we will do is create an exception class. This class will be used from within the data access layer of the WebAPI, caught by the controller, assigned to an HttpError for response, deserialized as json by the client, and utilzed therein. For our sample, exceptions are expected within the DAL only and, therefore, the exception class will handle specific information regarding the data access. Additionally, our sample WebAPI is for internal only consumption so we may include information we'd normally not publish to the general public. To that end, we will be implementing the following properties:

ServerName:
Since we're using a load balanced web farm for serving the WebAPI, the client may need to know the server name for further troubleshooting.
MethodName:
The name of the method being executed.
StoredProcedure:
The name of the stored procedure being executed by the method.

Create a custom exception class.

using System;
namespace SampleApp.WebAPI
{
    public class WebApiException : Exception
    {
        public string ServerName { get; set; }
        
        public string MethodName { get; set; }
        
        public string StoredProcedure { get; set; }
        public WebApiException(string message, Exception innerException, string methodName, string storedProcedure)
            : base(message, innerException)
        {
            ServerName = Environment.MachineName;
            MethodName = methodName;
            StoredProcedure = storedProcedure;
        }

        public void PopulateError(ref HttpError httpError)
        {
            //This is encapsulated here so that if/when we implement other types of exceptions for WebApi, 
            //the controller implementation can remain the same.
            httpError["ServerName"] = ServerName;
            httpError["MethodName"] = MethodName;
            httpError["StoredProcedure"] = StoredProcedure;
        }
    }
}

Utilize the custom exception in a data access method

For purposes of this sample, we're going to be returning a list of Addresses from a Sql Server database. We will wrap the data operations in a try/catch block and throw our new exception.

using SampleApp.WebAPI.DataModels;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
namespace SampleApp.WebAPI.DataAccess
{
    public static class AddressDAL
    {
        public static List<Address> GetAddresses()
        {
            List<Address> addresses = new List<Address>();
            try
            {
                using (SqlConnection cn = new SqlConnection(ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString))
                {
                    cn.Open();
                    using (SqlCommand cmd = cn.CreateCommand())
                    {
                        cmd.CommandText = "GetAddresses";
                        cmd.CommandType = CommandType.StoredProcedure;
                        using (SqlDataReader sdr = cmd.ExecuteReader())
                        {
                            while(sdr.Read())
                            {
                                addresses.Add(new Address(sdr));
                            }
                        }
                    }
                }
                catch (Exception exception)
                {
                    //Log exception here
                    throw new WebApiException(exception.Message, exception, "GetAddresses");
                }
                return addresses;
            }
        }
    }
}

Consume the custom exception and pass it back to the caller.

Here, we will handle the exception in the controller, create an HttpError and populate the values, then return the error to the client for consumption.

using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using SampleApp.WebAPI.DataModels;
using SampleApp.WebAPI.DataAccess;

namespace SampleApp.WebAPI.Controllers.AddressService
{
    public class AddressController : ApiController
    {
        public List<Address> GetAddresses()
        {
            try
            {
                return AddressDAL.GetAddresses();
            }
            catch (WebApiException exception)
            {
                HttpError httpError = new HttpError(exception.Message);
                exception.PopulateError(ref httpError);
                throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, httpError));
            }
        }
    }
}

Client implementation -- Custom Exceptions

First, we'll define the custom exceptions that we will use on the client side. We will be using two class, one to deserialize the response and another for upstream handling.

HttpError deserializer class

    
namespace SampleApp.Client
{
    // Errors that occur on the WebApi Server Side will generate data for this error container
    // Note: It is not descending from Exception and therefore is not named as Exception
    public class WebApiError
    {
        public string Message { get; set; }
        public string ServerName { get; set; }
        public string MethodName { get; set; }
        public string StoredProcedure { get; set; }
    }
}

Exception for client usage.

using System;
namespace SampleApp.Client
{
    public class WebApiClientException : Exception
    {
        public WebApiClientException() { }
        public WebApiClientException(string message) : base(message) { }
        public WebApiClientException(string message, Exception innerException) : base(message, innerException) { }
        public WebApiClientException(WebApiError error)
            : base(string.Format("WebAPI service call returned an exception. Message {0}; ServerName {1}; MethodName {2}; StoredProcedure {3}",
                error.Message, error.ServerName, error.MethodName, error.StoredProcedure))
        {
        }
    }
}

Client implementation -- generic webservice wrapper class

Using a generic wrapper class for calling webservices allows other code to more readily unit testable without the necessity of trying to fake the asynchronous calls preferred by the framework.

using System;
using System.Net.Http;
using System.Net.Http.Headers;

namespace SampleApp.Client
{
    public class WebApiClient : HttpClient
    {

        public WebApiClient()
        {
            DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        }

        public WebApiClient(string baseUrl) : this()
        {
            BaseAddress = new Uri(baseUrl);
        }

        public WebApiClient(string baseUrl, TimeSpan timeout):this(baseUrl)
        {
            Timeout = timeout;
        }

        public WebApiClient(TimeSpan timeout) : this(null, timeout)
        {
        }

        public TResultType Get<TResultType>(string url)
        {
            using (HttpResponseMessage response = GetAsync(url).Result)
            {
                if (response.IsSuccessStatusCode)
                {
                    return response.Content.ReadAsAsync<TResultType>().Resu<
                }
                WebApiError w = response.Content.ReadAsAsync<WebApiError>().Resu<
                throw new WebApiClientException(w);
            }
        }

        public TResultType Post<TResultType, TContentType>(string url, TContentType content)
        {
            using (HttpResponseMessage response = this.PostAsJsonAsync(url, content).Result)
            {
                if (response.IsSuccessStatusCode)
                {
                    return response.Content.ReadAsAsync<TResultType>().Resu<
                }
                WebApiError w = response.Content.ReadAsAsync<WebApiError>().Resu<
                throw new WebApiClientException(w);
            }
        }

        public void Post<TContentType>(string url, TContentType content)
        {
            using (HttpResponseMessage response = this.PostAsJsonAsync(url, content).Result)
            {
                if (!response.IsSuccessStatusCode)
                {
                    WebApiError w = response.Content.ReadAsAsync<WebApiError>().Resu<
                    throw new WebApiClientException(w);
                }
            }
        }

        public TResultType Delete<TResultType>(string url)
        {
            using (HttpResponseMessage response = DeleteAsync(url).Result)
            {
                if (response.IsSuccessStatusCode)
                {
                    return response.Content.ReadAsAsync<TResultType>().Resu<
                }
                WebApiError w = response.Content.ReadAsAsync<WebApiError>().Resu<
                throw new WebApiClientException(w);
            }            
        }

        public void Delete(string url)
        {
            using (HttpResponseMessage response = DeleteAsync(url).Result)
            {
                if (!response.IsSuccessStatusCode)
                {
                    WebApiError w = response.Content.ReadAsAsync<WebApiError>().Resu<
                    throw new WebApiClientException(w);
                }
            }
        }

        public TResultType Put<TResultType, TContentType>(string url, TContentType content)
        {
            using (HttpResponseMessage response = this.PutAsJsonAsync(url, content).Result)
            {
                if (response.IsSuccessStatusCode)
                {
                    return response.Content.ReadAsAsync<TResultType>().Resu<
                }
                WebApiError w = response.Content.ReadAsAsync<WebApiError>().Resu<
                throw new WebApiClientException(w);
            }
        }

        public void Put<TContentType>(string url, TContentType content)
        {
            using (HttpResponseMessage response = this.PutAsJsonAsync(url, content).Result)
            {
                if (!response.IsSuccessStatusCode)
                {
                    WebApiError w = response.Content.ReadAsAsync<WebApiError>().Resu<
                    throw new WebApiClientException(w);
                }
            }
        }
    }
}

Client Implementation - Final!

The final piece of the puzzle is the final consumption of the exception. What you decide to do with it in a production environment is beyond the scope of this document.

using System;
using System.Collections.Generic;
using System.Configuration;

namespace SampleApp.Client
{
    public static class AddressClient
    {
        public List<Address> GetAddresses()
        {
            try
            {
                using (WebApiClient client = new WebApiClient(ConfigurationManager.AppSettings["ServiceUrl"]))
                {
                    return client.Get<List<Address>>("v1/Addresses");
                }
            }
            catch (AggregateException e)
            {
                //This is the type of exception that occurs when the webservice times out
            }
            catch (WebApiClientExeption e)
            {
                //Here is a custom exception handling
                //probably want to log it, maybe want to display some message to the client
                //handle it per your necessity
            }
        }
    }
}

Summary

There is always a need to have additional information code consumable from an exception that occurs from remote services. Without explicit handling, the json deserializer will raise exceptions for no default serializer fount for text/html. While this information may be useful if you're browsing the service via a browser where html can be displayed, this information is completely useless in code. By wrapping the exception and returning an HttpError along with a failed request, we can inform the client about as much detail of what occurred as is needed. While this sample does not include returning an entire stack trace, there is nothing technical that prevents doing so. Further, all kinds of information we typically don't want to expose to the general public can readily be made availabe in the error context.

There is ample opportunity for improvement in the code presented herein and this code is not meant to be copy & paste ready for product usage without further customizations.