When writing ODATA controllers for our projects we spend quite some time on writing boilerplate code and input parameter validation. In addition, for our error handling we would like detailed information for internal troubleshooting but not so detailed information sent back to the client (i.e. no stack traces). In order to achieve this without extra work we started looking at Code Contracts from Microsoft Research that is part of the System.Diagnostics.Contracts namespace.

So in this article we will show you a way on how to use Code Contracts along with Web.Api to send custom HTTP status codes to the client while still maintaining full error information for internal troubleshooting.

With Code Contracts we can easily perform input parameter validation via Contract.Requires() at the start of any method as a precondition check. However, upon failure of such a precondition an exception of type System.Diagnostics.Contracts.__ContractsRuntime+ContractException is raised and then sent as an HTTP 500 to the client – along with a full stack trace.

However, in most cases we would actually like to send other status codes such as HTTP 400 Bad Request to the client, as it is not a server error but due to malformed data sent to the controller. In order to address this we register an exception filter via HttpConfiguration.Filters that will ‘catch’ this exception as it is sent on the Web.Api pipeline back to the client.

Unfortunately the ContractException does not have a public constructor so we cannot cast the generic exception from the HttpActionExecutedContext. Therefore we check its full name and act accordingly. In order to specify a custom status code we use the message parameter of the exception and define it as a piped string in the form |400|my custom error message|. The custom error message is optional. If not defined the actual precondition will be sent as the error message to the client.

So an example for a PUT operation could look like this:

// PUT: odata/Endpoints(5)
public IHttpActionResult Put([FromODataUri] int key, Endpoint endpoint)
{
    Contract.Requires(0 < key, "|400|");
    Contract.Requires(null != endpoint, "|400|");

    // actual code ...
}

When performing an operation with an empty body, the Contract.Requires() statement would fail as endpoint is null. The exception is then caught by our exception filter and the client will receive a response like this:

HTTP/1.1 400 Bad Request

{
  "odata.error":{
    "code":"","message":{
      "lang":"en-US","value":"Precondition failed: null != endpoint"
    }
  }
}

In case you want to provide a more user friendly or detailed error message you can adjust the Contract.Requires() statement to something like this: Contract.Requires(null != endpoint, "|400|Specify an endpoint to update|";. The error response will then look like this:

HTTP/1.1 400 Bad Request

{
  "odata.error":{
    "code":"","message":{
      "lang":"en-US","value":"Specify an endpoint to update"
    }
  }
}

The exception filter itself is pretty straightforward. As you can see we rewrite the Response to the client while logging the full stack trace to our internal logging system (along with the original HTTP request and the calling user):

public class ContractRequiresExceptionFilterAttribute : ExceptionFilterAttribute
{
  private const string CONTRACT_REQUIRES_EXCEPTION_FULLNAME = 
    "System.Diagnostics.Contracts.__ContractsRuntime+ContractException";

  public override void OnException(HttpActionExecutedContext context)
  {
    if (CONTRACT_REQUIRES_EXCEPTION_FULLNAME != 
          context.Exception.GetType().FullName)
    {
      return;
    }

    Trace.WriteException(context.Exception.Message, context.Exception);

    var ex = context.Exception;
    var exMessage = (null == ex.Message) ? String.Empty : ex.Message;
    var httpParams = exMessage.Split('|');
    if(1 >= httpParams.Length)
    {
      context.Response = context.Request.CreateErrorResponse(
          HttpStatusCode.InternalServerError, 
          context.Exception.Message, 
          context.Exception);
      return;
    }

    int statusCode;
    try
    {
      statusCode = Convert.ToInt32(httpParams[1].Trim());
    }
    catch
    {
      statusCode = 500;
    }
    string message;
    if(2 < httpParams.Length && 
        !String.IsNullOrWhiteSpace(httpParams[2].Trim()))
    {
      message = httpParams[2].Trim();
    }
    else
    {
      message = httpParams[0].Trim();
    }
    context.Response = context.Request
        .CreateErrorResponse((HttpStatusCode)statusCode, message);
  }
}

Registration of the filter is performed in WebApiConfig.cs as usual:

public static class WebApiConfig
{
  public static void Register(HttpConfiguration config)
  {
    // ...
    config.Filters.Add(new HttpStatusExceptionFilterAttribute());
    // ...
  }
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.