Friday, June 29, 2018

Web API Status Codes and Errors

In a previous post titled I'm in the future of the web and it doesn't work I complained in general about how "the web" is so old, so primitive and so full of hacks that writing applications for it is a nightmare. One pet hate of mine is REST, a convention that has unfortunately gained worldwide popularity in recent years. It's important to be reminded that REST is not a protocol, it is a convention, and a vague one at that. You have to invent your own convention of how to encode information in the HTTP requests and responses, and how to handle the various unpredictable HTTP response status codes you may receive.

In early Web API services that I wrote, I naively attempted to coerce the business logic results into HTTP status codes. So for example, getting a database row that's not found would return a 404 (NotFound), or creating a new row would return 201 (Created), or deleting a row would return 204 (NoContent).

I soon realised that most of the HTTP status codes have meanings that are peculiar to HTTP and have nothing to do with typical business logic. For example, if someone makes a service call with a bad parameter, what do you return? Perhaps a 400 (BadRequest) seems appropriate? Well no, because the request was not "bad" according to the way a 400 is defined: it was not too large, it didn't have malformed syntax, nor did it have deceptive routing (see List of Status Codes). Your request actually worked correctly, it's just the business logic that considers it a "bad request".

Web searches will reveal endless contradictory arguments and claims about how data and status information should be round-tripped using REST. I'm so sick of the arguments and complicated code that I have throw in the towel and adopted my own simple convention. I have in-fact hijacked REST and turned it into the carrier for a simple protocol.

Only these status codes are returned by my services.

200 (OK)
The request was successfully processed by the application without any exception being thrown. Some extra information is needed to determine if the processing succeeded of failed in the "business" sense, and I have a convention for that. See the 200 section below.

500 (Internal Server Error)
All unhandled exceptions are converted into this status code. See the 500 section below.

Other
Anything else is unexpected and is regarded as a fatal problem that is probably outside of the control of the application. Things like an incorrect Uri, a misconfigured server, a security problem, and so on, can result in all sorts of weird status codes, and I just don't care as it means something is really stuffed up and needs special attention. Calling applications may decide to abort and show a "pink screen" in these cases.

Having just a few meaningful conditions greatly simplifies the internal coding to issue a request and inspect a response.

200 OK

Indicates a response was generated without any unhanded exceptions during processing, however, more information may be needed to determine what happened inside the business logic.

Deleting a database row is an example where you may need to know if anything was actually deleted. Adding a row may need to report if the row was inserted or updated. In cases like this you need more information. My .NET coding convention is to have a response base class like this skeleton:

public class ResponseBase
{
  public bool Success { get; set; }
  public int? Code { get; set; }    // could be an enum
  public string Message { get; set; }
}

Every class that can be serialized into a response body is derived from this class. So values in the Success, Code and Message (or whatever you like) properties can report in more detail what happened along with any real response data. This provides extra consistent information in all responses from your application.

The extra response information could be placed out-of-band in the headers for example, but I find it simpler to have them in the body.

500 (Internal Server Error)

In .NET Web API projects you can use the OnException handler to intercept all unhandled exceptions (see Exception Handling in Web API). I convert all exceptions into a 500 response using skeleton code like this:

var ex = context.Exception.GetBaseException();
var error = new HttpError(ex.Message);
error.ExceptionType = ex.GetType().Name;
error.MessageDetail = ex.StackTrace;
context.Response = context.Request.CreateErrorResponse(HttpStatusCode.InternalServerError, error);

In service calling code you can now be certain that all 500 responses contain consistent helpful information about the unhandled exception. The response can be serialized into a class like this:

public sealed class InternalError
{
  public string Message { get; set; }
  public string ExceptionType { get; set; }
  public string MessageDetail { get; set; }
}

You can then decide if the response is "fatal" or not and take appropriate action.

Summary

By reducing REST responses down to two status codes and expanding the 200 response with extra information, it could be argued that I am hijacking REST and converting it into a toy protocol. I can't argue with that, but as an application developer I really need a protocol to move data back and forth in a consistent manner. REST is just a style or a convention and the vast majority of HTTP status codes are utterly meaningless to applications, so it was basic instinct that drove me to use REST the way I've described in this blog post.

For practical purposes, I just need to send and receive serialized information, and it either works or it doesn't. I don't give a toss about idempotency, url-based resources or the zoo of strange archaic response status codes that are defined, I just want to send and receive SOAP-like envelopes of information.

The fact that I'm doing this hints that there is something deficient or inappropriate about REST.

No comments:

Post a Comment