Error Handling in APIs - PHP Web Services (2013)

PHP Web Services (2013)

Chapter 12. Error Handling in APIs

Errors are a fact of life. Users will enter nonsense into your system, not because they are simpletons (although it does often look that way), but because their expectations and understanding are different from yours. The Internet is a very loosely-coupled affair and all kinds of things can and will go wrong at a technical level, once in a while. How your API handles these inevitable situations is a measure of the quality and design of your API, so this chapter gives some pointers on what to look out for and how to do it well.

Output Format

This is the golden rule: always respond in the format that the client was expecting. This means that it is never acceptable to return an HTML error message when the client expected JSON (in fact, in certain PHP versions, passing invalid JSON to json_decode() causes a segment fault!). If your system does return HTML messages when things go wrong, that is a bug and needs fixing. If an unexpected format is sent, the client will not be unable to understand the response and any error information contained in it.

In order to handle this requirement, there are some established patterns when designing our API that may help. Many modern applications have some kind of “front controller” pattern, in which all incoming requests are handled by a common entry point. This common front controller typically parses the request and figures out which part of the system it should be passed on to. We can put the same ideas into practice at the end of the request and make sure that the data to send back to the client always passes through a common point. At this point, it is possible to put in an output handler to correctly and consistently format the outgoing data correctly.

Here’s a very simple front controller to give an idea of how this might look; you might use something along these lines, or follow the conventions of whichever framework you use:

<?php

require "accept.php";

spl_autoload_register(function ($classname) {

require ("inc/" . strtolower($classname) . ".php");

});

// create the correct view format

$accepted_formats = parseAcceptHeader();

$supported_formats = array("application/json", "application/xml");

foreach($accepted_formats as $format) {

if(in_array($format, $supported_formats)) {

// yay, use this format

break;

}

}

switch($format) {

case "application/xml":

$view = new XmlView();

break;

case "application/json":

default:

$view = new JsonView();

break;

}

set_exception_handler(function ($exception) use ($view) {

$data = array("message" => $exception->getMessage());

if($exception->getCode()) {

$view->status = $exception->getCode();

} else {

$view->status = 500;

}

$view->render($data);

});

// allowed controllers

$controllers = array("user", "post", "category");

// parse URL, first is class, then function

$pieces = explode('/', $_SERVER['PATH_INFO']);

if(in_array($pieces[1], $controllers)) {

$classname = $pieces[1];

$functionname = $pieces[2];

$class = new $classname();

$data = $class->$functionname();

$view->render($data);

} else {

throw new Exception("request not recognised", 400);

}

There’s a lot happening here, so let’s run through it in pieces. The first block is just the autoloader. To keep the example short, you will find all the classes in this example application are in /inc with lowercase file names matching their CamelCase class names. Next, the Accept header is parsed and the correct format is established using the example from the headers chapter. We work out the correct return format first, so it is then possible to return meaningful and understandable error information to a user in the event of any issues.

Next is the exception handler; this is a key pawn in our game of returning excellent and correctly-formatted content. Rather than passing error statuses and messages up and down potentially deep stacks, the application can be designed to use exceptions instead. Whenever something goes wrong, an exception will be passed up the stack and, if not handled, will arrive at the exception handler. This feature can be used to make sure that the only output from an error will be handled in the way the requesting client expects; care must be taken, however, to ensure that the messages returned by the exceptions are appropriate for public view—perhaps by always throwing specific exceptions with humane messages. A closure is used to bake the $view object into the exception handler, so that our output will be in the correct format.

Many applications follow patterns that separate out various pieces of functionality. You may have a system of routes and templates, where different URL patterns will be parsed to figure out which code to run. It’s also very common to have an MVC (Model, View, Controller) style of application architecture; in an API this works very well, although the V (for view) becomes more like an output handler than a series of per-page templates. Here, we take the view that the first part of the URL will be the class name where the code can be found (in MVC, the “controller”), and the second part will be the function to actually run (in MVC, this is referred to as the “action”). When using a similar approach in your own code, do make sure that you are filtering incoming values correctly; the example uses the raw URL pieces for brevity, but a public-facing application would have much tighter security measures and would check that both class and function exist. Exactly the same principles apply to all incoming data; the URL, headers including cookies, and the POST variables (for example) are all to be treated with the same suspicion before using them in your code.

The biggest departure from a standard web architecture shows up right at the end of this front controller, where we send (sometimes called “dispatch”) the request to the location where the code that can form the response is. Many of those controller-type patterns will then pass the program flow onto a template, and execution will end once the template has been rendered. In this example, those functions instead return data back to where the calls fan out from in order to be passed through a common output handler as the last step in the process.

The output handlers themselves can be beyond simple. To illustrate my point, here’s the JsonView class used by the previous example code:

<?php

class JsonView

{

public $status;

public function render($data) {

if($this->status) {

http_response_code($this->status);

}

header('Content-Type: application/json; charset=utf8');

echo json_encode($data);

return true;

}

}

The XML equivalent is slightly longer at 24 lines of code, but still not complex at all. The main things to remember are to set the correct Content-Type header and body format. Remember also that the status code needs to be set appropriately, depending on what action was performed.

Meaningful Error Messages

We all know how frustrating it is to get error messages from systems that say something like “an unknown error has occurred.” This gives us absolutely no information at all on how we can coax the application to behave better. Even worse is an application I work with regularly, which will return the error message “Invalid permissions!” in the event that anything at all goes wrong, regardless of whether or not there is a problem with permissions. This leads to people looking in completely the wrong places for solutions and eventually filing very frustrated support tickets.

Error messages should be more than a tidy placeholder that the developer can use to find where in the code she should look when a bug is reported (there is also something to be said in favor of avoiding any copying and pasting of error messages for this reason). The information that an application returns in the event of an error is what lies between the application, the user, and the bug-reporting software. Anyone trying to use an application will have something he is trying to achieve and will be motivated to achieve that goal. If the application can return information about what exactly went wrong, then the user will adjust his attempts and try again, without bothering you. Users tend not to read documentation (developers in particular will usually only read instructions once something isn’t working—all engineers do this), so the error information is what forms their experience of the system.

When something goes wrong, answer your user’s questions:

§ Was a parameter missing or invalid? Was there an unexpected parameter? (A typo can make these two questions arise together very regularly.)

§ Was the incoming format invalid? Was it malformed or is it in a format the server does not accept?

§ Was a function called that does not exist? (For common mistakes, you might even suggest what the user may have meant.)

§ Does the system need to know who the user is before granting access? Or is this user authenticated but with insufficient privileges?

When it exists, give information about which fields are the problem, what is wrong with them, or if something is missing. It is also very helpful to users if you can collate the errors as much as possible. Sometimes, errors prevent us from proceeding any further with a request, but if, for example, one of the data fields isn’t valid, we could check all the other data fields and return that information all at once. This saves the user from untangling one mistake only to trip straight over the next one, and also shows if the errors are related and could all be fixed in one go.

What to Do When You See Errors

Let us consider our other role in that relationship: that of the consumer of a service. Many of the APIs we work with are not ones we made ourselves, so inevitably we will be encountering some of the behaviors this chapter preaches against. What can we do when this happens? The best approach is to take baby steps.

First, go back to the last known good API call. At the very early stages of working with an API, that means reading the documentation or finding a tutorial to follow, and seeing if you can make any calls at all against this system. Some APIs offer what I call a “heartbeat” method and some offer a status page. Look for something that doesn’t need authentication or any complicated parameters to call, and which will let you know that the API is actually working and the problem is at your end. Flickr has a particularly good example of this with their flickr.test.echo method.

Once it has been established that the target API is working, take a look at the call that was being attempted. Does it have any required parameters? Can the call be made in its simplest possible form, passing the smallest possible amount of data with the call? Even once things seem to be improving, it is advisable to approach changes to the API call in small steps, changing data format or adding a parameter, then checking that the response comes back as expected. Just like any kind of debugging, this iterative approach will help to pinpoint which change caused an error to occur.

While these test requests are being made, regardless of which tool is being used, take care to check the headers of the response as well as the body. Status codes, Content-Type headers, cache information, and all kinds of other snippets can be visible in the header and give clues about what is happening.

Do take the time to use the tools and tactics available to you, whether the errors are in your API or someone else’s. In particular, the techniques covered in Chapter 9 will be superbly useful in such scenarios.