RESTful Services and Business Exceptions

Last week I had the privilege of speaking at MIX09.  My session RESTful Services for the Programmable Web with Windows Communication Foundation is already available for online viewing (pretty cool how fast they turned that around).  This morning I got an email from Francois who saw the session on the web and he had an excellent question.

“I just wanted to know how you deal with business exceptions (concurrency, invalid state transitions, ...) when an update or delete is performed. Do you simply return HTTP error codes? How do you do the equivalent of returning a Soap Fault?”

Great question, to answer it let me first lay some foundation then consider the second part of your question with regard to SOAP faults and finally use some examples to hopefully make this more clear.

REST and HTTP Status Codes

One of the key values of REST is that you are using a universal API known as HTTP.  Because of this you must try to understand the way that HTTP wants you to deal with this.  And how does HTTP want me to deal with this issue?  By using an HTTP Status code.

The first line of an HTTP response is the status line – HTTP clients look to this line to understand the outcome of their request.  It isn’t just a matter of errors, there are many different response codes you could return or encounter when invoking RESTful services.    (For more information on the Status-Line see the HTTP spec, section 6.1)

        Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF

The status code comes from a long list of codes, according to the spec, the list of codes is extensible but you should follow the model based on the categories of errors

       - 1xx: Informational - Request received, continuing process
      - 2xx: Success - The action was successfully received,
        understood, and accepted
      - 3xx: Redirection - Further action must be taken in order to
        complete the request
      - 4xx: Client Error - The request contains bad syntax or cannot
        be fulfilled
      - 5xx: Server Error - The server failed to fulfill an apparently
        valid request

Even so, I would not go about creating custom HTTP status codes.  Instead I would try to use the commonly used status codes listed in section 10 of the HTTP spec.

REST equivalent of SOAP faults

The designers of SOAP had exceptions in mind when they created SOAP faults.  Previous distributed computing environments (DCOM, CORBA etc.) did not have a model for propagating exceptions across boundaries.  SOAP came along 10+ years after these technologies a time during which exceptions became accepted as a better way of dealing with errors.  Many SOAP stacks (WCF being the prime example) will catch an exception, turn it into a SOAP fault and pass it along to the client side which will unpack the message and throw an exception on the client side.

SOAP faults are different than HTTP status codes because they are always errors and there is no taxonomy of errors, just whatever you want them to be.  In other words, the “error protocol” is application specific.

In REST HTTP status codes are common and they are not always errors.  However the errors do fall into 2 categories 4XX (Client) and 5XX server errors.   Now let’s consider some common scenarios where you must deal with errors and status codes.

Error Scenarios

The first class of errors you must consider are (4XX) client request errors.  Generally it means that the server rejected the request for some reason.

Get or Update record that does not exist

Client does an HTTP GET for a resource ID that does not exist in the database.  On the server you might call into the data layer to update a record an get a KeyNotFoundException (like I did in the video).  What happens if you do nothing?  Well WCF will catch all unhandled exceptions and return a status code of 500 - “Internal Server Error”.  This is not what you want, because it implies that the problem is a server problem. 

In this case you should return a HTTP status code of 400 404 “Not Found”  (edit – 3/25 – thanks Clemens!)

10.4.1 400 Bad Request

The request could not be understood by the server due to malformed syntax. The client SHOULD NOT repeat the request without modifications.

Here is some code that processes a GET request by looking it up in a dictionary.  This is the fixed version of what I showed in the video.  As you can see I’m converting an exception into a status code by throwing a WebProtocolException.

 [OperationContract]
[WebGet(UriTemplate = "/{id}")]

SessionData GetSession(string id)
{
    try
    {
        return _sessions[id];
    }
    catch (KeyNotFoundException)
    {
        throw new WebProtocolException(
            HttpStatusCode.NotFound);
    }
}

Add a resource with missing or invalid data

Client does an HTTP POST with missing or invalid data in the request body.  The server attempts to validate the request and rejects it because of the invalid or missing data.  The HTTP status code should be 400 – Bad request

Server Processing Errors

Suppose that the request is valid, the resource exists and for whatever reason (concurrency or some other processing error) the server cannot complete the request.  Then what?

10.5 Server Error 5xx

Response status codes beginning with the digit "5" indicate cases in which the server is aware that it has erred or is incapable of performing the request. Except when responding to a HEAD request, the server SHOULD include an entity containing an explanation of the error situation, and whether it is a temporary or permanent condition. User agents SHOULD display any included entity to the user. These response codes are applicable to any request method.

You should return a HTTP status code of 500 along with some text to let the user know what is happening (if you want to).  Here is an example of code that would do this kind of processing. 

 [OperationContract]
[WebInvoke(Method = "PUT", UriTemplate = "/{id}")]

SessionData UpdateSession(string id, SessionData updatedSession)
{
    try
    {
        if (!_sessions.Contains(updatedSession.ID))
            throw new WebProtocolException(HttpStatusCode.BadRequest);
        if (!_sessions.IsValidSession(updatedSession))
            throw new WebProtocolException(HttpStatusCode.BadRequest);
        return _sessions.Update(updatedSession);
    }
    catch (TimeoutException)
    {
        throw new WebProtocolException(HttpStatusCode.InternalServerError,"Update session timeout",null);
    }
}

WebProtocolException does provide mechanisms for passing more detail in a SOAP Fault like way as a part of the HTTP response body.  Sounds like a good topic for my next post.