Каков наилучший способ создания модели ответов на ошибки REST API и системы кодов ошибок?

15

Моя реализация REST вернет ошибки в JSON со следующей структурой:

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

Я предлагаю создать специальную модель ответа, в которой я могу передать необходимые значения свойств (dev_message, message_for_user, some_internal_error_code) и вернуть их. В коде это будет похоже на это:

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

Как должна выглядеть эта модель? Должен ли я реализовать методы, например successResponse (), где я буду передавать только текстовую информацию, и код будет по умолчанию 200 там? Я застрял с этим. И это первая часть моего вопроса: мне нужно реализовать эту модель, это хорошая практика? Потому что сейчас я просто возвращаю массивы прямо из кода.

Вторая часть о системе кодов ошибок. Коды ошибок будут описаны в документации. Но проблема, с которой я сталкиваюсь, заключается в коде. Каков наилучший способ управления кодами ошибок? Должен ли я написать их внутри модели? Или было бы лучше создать отдельный сервис для обработки этого?

ОБНОВЛЕНИЕ 1

Я реализовал модель класса для ответа. Это аналогичный ответ Грега, та же логика, но, кроме того, я жестко закодировал написанные ошибки в модели, и вот как это выглядит:

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

     ...some code...
    }

Почему я это сделал? А зачем?

  1. Это выглядит круто в коде: return new ErrorResponse(ErrorResponse::SOME_ENTITY_NOT_FOUND );
  2. Легко изменить сообщение об ошибке. Все сообщения находятся в одном месте вместо контроллера / службы / и т. Д. Или чего-либо еще, что вы поместили.

Если у вас есть предложения по улучшению, пожалуйста, прокомментируйте.

Grokking
источник

Ответы:

13

В этой ситуации я всегда сначала думаю об интерфейсе, а затем пишу код PHP для его поддержки.

  1. Это REST API, поэтому значимые коды состояния HTTP просто необходимы.
  2. Вы хотите, чтобы согласованные, гибкие структуры данных отправлялись клиенту и от него.

Давайте подумаем обо всех вещах, которые могут пойти не так, и их кодах статуса HTTP:

  • Сервер выдает ошибку (500)
  • Ошибка аутентификации (401)
  • Запрашиваемый ресурс не найден (404)
  • Изменяемые данные были изменены с момента загрузки (409)
  • Ошибки проверки при сохранении данных (422)
  • Клиент превысил свой запрос (429)
  • Неподдерживаемый тип файла (415)

Обратите внимание, что есть другие, которые вы можете исследовать позже.

Для большинства условий сбоя возвращается только одно сообщение об ошибке. 422 Unprocessable EntityОтвет, который я использовал для «ошибки проверки» может вернуть более одной ошибки --- Один или несколько ошибок в поле формы.

Нам нужна гибкая структура данных для ответов об ошибках.

Возьмите в качестве примера 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!"
        ]
    }
}

Сравните это с простыми ошибками проверки при попытке отправить что-то на сервер:

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"
        ]
    }
}

Ключевым моментом здесь является тип контента text/json. Это сообщает клиентским приложениям, что они могут декодировать тело ответа с помощью JSON-декодера. Если, скажем, внутренняя ошибка сервера не обнаружена и вместо этого доставляется ваша общая веб-страница «Что-то пошло не так», тип контента должен быть text/html; charset=utf-8таким, чтобы клиентские приложения не пытались декодировать тело ответа как JSON.

Это выглядит все найти и модно, пока вам не нужно поддерживать ответы JSONP . Вы должны вернуть 200 OKответ, даже за сбои. В этом случае вам нужно будет определить, что клиент запрашивает ответ JSONP (обычно путем определения вызываемого параметра запроса URL callback), и немного изменить структуру данных:

(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!"
            ]
        }
    }
});

Тогда обработчик ответа на клиенте (в веб-браузере) должен иметь глобальную функцию JavaScript, displayBlogPostкоторая принимает один аргумент. Эта функция должна определить, был ли ответ успешным:

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

Итак, мы позаботились о клиенте. Теперь позаботимся о сервере.

<?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;
    }
}

И использовать это в случае ошибки сервера:

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

Или при проверке ввода пользователя:

// 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;

После этого вам просто нужно что-то, что возьмет возвращенный объект ответа и преобразует его в JSON и отправит ответ по-своему.

Грег Бургардт
источник
Спасибо за ответ! Я реализовал подобное решение. Единственная разница в том, что я не пропускаю никаких сообщений самостоятельно, они уже заданы (см. Мой обновленный вопрос).
Гроккинг
-2

Я столкнулся с чем-то похожим, я сделал 3 вещи,

  1. Создал для себя ExceptionHandler под названием ABCException.

Так как я использую Java & Spring,

Я определил это как

 public class ABCException extends Exception {
private String errorMessage;
private HttpStatus statusCode;

    public ABCException(String errorMessage,HttpStatus statusCode){
            super(errorMessage);
            this.statusCode = statusCode;

        }
    }

Затем назвал это, где требуется, как это,

throw new ABCException("Invalid User",HttpStatus.CONFLICT);

И да, вам нужно сделать ExceptionHandler в вашем контроллере, если вы используете веб-сервис на основе REST.

Аннотируйте это с @ExceptionHandlerпомощью Spring

Дэйв Ранджан
источник
Программисты о концептуальных вопросах, и ответы должны объяснить вещи . Создание дампов кода вместо объяснения похоже на копирование кода из IDE на доску: это может показаться знакомым и даже иногда понятным, но это кажется странным ... просто странным. У доски нет компилятора
gnat