REST API – Best Way to Create Error Response Model and Error Codes

apimvcPHPrest

My REST implementation will return errors in JSON with next structure:

{
 "http_response":400,
 "dev_message":"There is a problem",
 "message_for_user":"Bad request",
 "some_internal_error_code":12345
}

I suggest to create special response model, where I can pass needed values for properties(dev_message, message_for_user, some_internal_error_code), and return them. In code it would be similar to this:

$responseModel = new MyResponseModel(400,"Something is bad", etc...);

How this model should look like? Should I implement methods e.g. successResponse() where I'll pass only text information, and code will be default 200 there? I'm stuck with this.
And this is first part of my question: do I need to implement this model, is this good practise? Because for now, I'm just returning arrays directly from code.

The second part is about error code system. Error codes will be described in documentation. But the problem I'm encountering is in code.
What is the best way to manage error codes? Should I write them inside model? Or it would be better to create separate service for handling this?

UPDATE 1

I've implemented model class for response. It is similar Greg's answer, the same logic, but additionaly I have hardcoded written errors in model and here it is how it looks like:

    class ErrorResponse
    {
     const SOME_ENTITY_NOT_FOUND = 100;
     protected $errorMessages = [100 => ["error_message" => "That entity doesn't exist!"]];

     ...some code...
    }

Why I did this? And what for?

  1. It's looks cool in code:
    return new ErrorResponse(ErrorResponse::SOME_ENTITY_NOT_FOUND );
  2. Easy to change error message. All messages are in one place instead of controller/service/etc or whatever you'll placed it.

If you have any suggestions to improve this, please, comment.

Best Answer

In this situation, I always think of the interface first, then write PHP code to support it.

  1. It's a REST API, so meaningful HTTP status codes are a must.
  2. You want consistent, flexible data structures being sent to and from the client.

Let's think of all the things that could go wrong and their HTTP status codes:

  • The server throws an error (500)
  • Authentication failure (401)
  • The resource requested was not found (404)
  • The data you are modifying has been changed since you loaded it (409)
  • Validation errors when saving data (422)
  • The client has exceeded their request rate (429)
  • Unsupported file type (415)

Note, there are others which you can research later.

For most of the failure conditions, there is only one error message to be returned. The 422 Unprocessable Entity response, which I've used for "validation errors" could return more than one error --- One or more errors per form field.

We need a flexible data structure for error responses.

Take as an example, the 500 Internal Server Error:

HTTP/1.1 500 Internal Server Error
Content-Type: text/json
Date: Fri, 16 Jan 2015 17:44:25 GMT
... other headers omitted ...

{
    "errors": {
        "general": [
            "Something went catastrophically wrong on the server! BWOOP! BWOOP! BWOOP!"
        ]
    }
}

Contrast that with simple validation errors when trying to POST something to the server:

HTTP/1.1 422 Unprocessable Entity
Content-Type: text/json
Date: Fri, 16 Jan 2015 17:44:25 GMT
... other headers omitted ...

{
    "errors": {
        "first_name": [
            "is required"
        ],
        "telephone": [
            "should not exceed 12 characters",
            "is not in the correct format"
        ]
    }
}

They key here is the content type being text/json. This tells client applications that they can decode the response body with a JSON decoder. If, say, an internal server error isn't caught and your generic "Something went wrong" web page gets delivered instead, the content type should be text/html; charset=utf-8 so client applications won't attempt to decode the response body as JSON.

This looks all find and dandy, until you need to support JSONP responses. You must return a 200 OK response, even for failures. In this case you'll have to detect that the client is requesting a JSONP response (usually by detecting a URL request parameter called callback) and change up the data structure a bit:

(GET /posts/123?callback=displayBlogPost)

<script type="text/javascript" src="/posts/123?callback=displayBlogPost"></script>

HTTP/1.1 200 OK
Content-Type: text/javascript
Date: Fri, 16 Jan 2015 17:44:25 GMT
... other headers omitted ...

displayBlogPost({
    "status": 500,
    "data": {
        "errors": {
            "general": [
                "Something went catastrophically wrong on the server! BWOOP! BWOOP! BWOOP!"
            ]
        }
    }
});

Then the response handler on the client (in a web browser) should have a global JavaScript function called displayBlogPost which accepts a single argument. This function would have to determine if the response was successful:

function displayBlogPost(response) {
    if (response.status == 500) {
        alert(response.data.errors.general[0]);
    }
}

So we've taken care of the client. Now, let's take care of the server.

<?php

class ResponseError
{
    const STATUS_INTERNAL_SERVER_ERROR = 500;
    const STATUS_UNPROCESSABLE_ENTITY = 422;

    private $status;
    private $messages;

    public function ResponseError($status, $message = null)
    {
        $this->status = $status;

        if (isset($message)) {
            $this->messages = array(
                'general' => array($message)
            );
        } else {
            $this->messages = array();
        }
    }

    public function addMessage($key, $message)
    {
        if (!isset($message)) {
            $message = $key;
            $key = 'general';
        }

        if (!isset($this->messages[$key])) {
            $this->messages[$key] = array();
        }

        $this->messages[$key][] = $message;
    }

    public function getMessages()
    {
        return $this->messages;
    }

    public function getStatus()
    {
        return $this->status;
    }
}

And to use this in the case of a server error:

try {
    // some code that throws an exception
}
catch (Exception $ex) {
    return new ResponseError(ResponseError::STATUS_INTERNAL_SERVER_ERROR, $ex->message);
}

Or when validating user input:

// Validate some input from the user, and it is invalid:

$response = new ResponseError(ResponseError::STATUS_UNPROCESSABLE_ENTITY);
$response->addMessage('first_name', 'is required');
$response->addMessage('telephone', 'should not exceed 12 characters');
$response->addMessage('telephone', 'is not in the correct format');

return $response;

After that, you just need something that takes the returned response object and converts it into JSON and sends the response on its merry way.

Related Topic