1. Обзор
REST — это архитектура без сохранения состояния, в которой клиенты могут получать доступ к ресурсам на сервере и управлять ими. Как правило, службы REST используют HTTP для объявления набора ресурсов, которыми они управляют, и предоставляют API, который позволяет клиентам получать или изменять состояние этих ресурсов.
В этом руководстве мы узнаем о некоторых передовых методах обработки ошибок REST API, включая полезные подходы к предоставлению пользователям соответствующей информации, примеры с крупных веб-сайтов и конкретную реализацию с использованием примера приложения Spring REST.
2. Коды состояния HTTP
Когда клиент делает запрос к HTTP-серверу — и сервер успешно получает запрос — сервер должен уведомить клиента, был ли запрос успешно обработан или нет.
HTTP выполняет это с помощью пяти категорий кодов состояния:
- 100-й уровень (информационный) — сервер подтверждает запрос
- 200-й уровень (Успех) — сервер выполнил запрос, как и ожидалось.
- 300-й уровень (перенаправление) — клиенту необходимо выполнить дальнейшие действия для выполнения запроса
- 400-й уровень (ошибка клиента) — клиент отправил неверный запрос
- 500-й уровень (ошибка сервера) — серверу не удалось выполнить действительный запрос из-за ошибки на сервере.
На основе кода ответа клиент может предположить результат конкретного запроса.
3. Обработка ошибок
Первым шагом в обработке ошибок является предоставление клиенту надлежащего кода состояния. Кроме того, нам может потребоваться предоставить дополнительную информацию в теле ответа.
3.1. Основные ответы
Самый простой способ обработки ошибок — ответить соответствующим кодом состояния.
Вот некоторые распространенные коды ответов:
- 400 Bad Request — клиент отправил неверный запрос, например, отсутствует требуемый текст запроса или параметр.
- 401 Unauthorized — клиент не прошел аутентификацию на сервере.
- 403 Forbidden — клиент прошел аутентификацию, но не имеет разрешения на доступ к запрошенному ресурсу.
- 404 Not Found — запрошенный ресурс не существует
- 412 Precondition Failed — одно или несколько условий в полях заголовка запроса оцениваются как ложные.
- 500 Internal Server Error — на сервере произошла общая ошибка.
- 503 Service Unavailable — запрошенная услуга недоступна.
Несмотря на простоту, эти коды позволяют клиенту понять общий характер возникшей ошибки. Мы знаем, что если мы получаем, например, ошибку 403, нам не хватает прав доступа к запрошенному нами ресурсу. Тем не менее, во многих случаях нам необходимо предоставить дополнительную информацию в наших ответах.
500 ошибок сигнализируют о том, что при обработке запроса на сервере возникли проблемы или исключения. Как правило, эта внутренняя ошибка не является делом нашего клиента.
Поэтому, чтобы свести к минимуму подобные ответы клиенту, мы должны усердно пытаться обрабатывать или обнаруживать внутренние ошибки и по возможности отвечать другими соответствующими кодами состояния.
Например, если возникает исключение из-за того, что запрошенный ресурс не существует, мы должны показать это как ошибку 404, а не 500.
Это не означает, что 500 никогда не следует возвращать, а только то, что его следует использовать в непредвиденных условиях, таких как сбой службы, которые не позволяют серверу выполнить запрос.
3.2. Ответы на ошибки Spring по умолчанию
Эти принципы настолько вездесущи, что Spring кодифицировал их в механизме обработки ошибок по умолчанию .
Для демонстрации предположим, что у нас есть простое приложение Spring REST, которое управляет книгами, с конечной точкой для получения книги по ее идентификатору:
curl -X GET -H "Accept: application/json" http://localhost:8082/spring-rest/api/book/1
Если нет книги с идентификатором 1, мы ожидаем, что наш контроллер выдаст исключение BookNotFoundException
.
Выполняя GET на этой конечной точке, мы видим, что это исключение было сгенерировано, и это тело ответа:
{
"timestamp":"2019-09-16T22:14:45.624+0000",
"status":500,
"error":"Internal Server Error",
"message":"No message available",
"path":"/api/book/1"
}
Обратите внимание, что этот обработчик ошибок по умолчанию включает метку времени возникновения ошибки, код состояния HTTP, заголовок ( поле ошибки
), сообщение, если сообщения включены в ошибке по умолчанию (и по умолчанию пусто), и URL-адрес. где произошла ошибка.
Эти поля предоставляют клиенту или разработчику информацию, помогающую устранить проблему, а также представляют собой несколько полей, образующих стандартные механизмы обработки ошибок.
Также обратите внимание, что Spring автоматически возвращает код состояния HTTP 500, когда выдается наше исключение BookNotFoundException
. Хотя некоторые API будут возвращать код состояния 500 или другие общие коды, как мы увидим с API Facebook и Twitter, для простоты для всех ошибок лучше использовать наиболее конкретный код ошибки, когда это возможно.
В нашем примере мы можем добавить @ControllerAdvice
, чтобы при возникновении исключения BookNotFoundException
наш API возвращал статус 404 для обозначения Not Found
вместо 500 Internal Server Error
.
3.3. Более подробные ответы
Как видно из приведенного выше примера Spring, иногда кода состояния недостаточно, чтобы показать особенности ошибки. При необходимости мы можем использовать тело ответа, чтобы предоставить клиенту дополнительную информацию.
Предоставляя подробные ответы, мы должны включать:
- Ошибка – уникальный идентификатор ошибки.
- Сообщение – краткое сообщение, понятное человеку.
- Detail — более подробное объяснение ошибки.
Например, если клиент отправляет запрос с неправильными учетными данными, мы можем отправить ответ 401 с этим телом:
{
"error": "auth-0001",
"message": "Incorrect username and password",
"detail": "Ensure that the username and password included in the request are correct"
}
Поле ошибки
не должно совпадать с кодом ответа. Вместо этого это должен быть код ошибки, уникальный для нашего приложения. Как правило, для поля ошибки
не существует соглашения , ожидайте, что оно будет уникальным.
Обычно это поле содержит только буквенно-цифровые и соединительные символы, такие как тире или подчеркивание. Например, 0001
, auth-0001
и неверный-пользователь-пароль
являются каноническими примерами кодов ошибок.
Часть тела сообщения
обычно считается презентабельной в пользовательских интерфейсах. Следовательно, мы должны перевести это название, если мы поддерживаем интернационализацию . Поэтому, если клиент отправляет запрос с заголовком Accept-Language
, соответствующим французскому языку, значение заголовка
должно быть переведено на французский язык.
Детальная часть
предназначена для использования разработчиками клиентов, а не конечным пользователем , поэтому перевод не требуется.
Кроме того, мы также можем предоставить URL-адрес, например поле справки
, по которому клиенты могут перейти, чтобы узнать больше информации:
{
"error": "auth-0001",
"message": "Incorrect username and password",
"detail": "Ensure that the username and password included in the request are correct",
"help": "https://example.com/help/error/auth-0001"
}
Иногда мы можем захотеть сообщить о более чем одной ошибке для запроса.
В этом случае мы должны вернуть ошибки списком:
{
"errors": [
{
"error": "auth-0001",
"message": "Incorrect username and password",
"detail": "Ensure that the username and password included in the request are correct",
"help": "https://example.com/help/error/auth-0001"
},
...
]
}
И когда возникает одиночная ошибка, мы отвечаем списком, содержащим один элемент.
Обратите внимание, что ответ с несколькими ошибками может быть слишком сложным для простых приложений. Во многих случаях достаточно указать первую или самую значительную ошибку.
3.4. Стандартизированные органы реагирования
Хотя большинство REST API следуют схожим соглашениям, особенности обычно различаются, включая имена полей и информацию, включенную в тело ответа. Эти различия мешают библиотекам и платформам одинаково обрабатывать ошибки.
Стремясь стандартизировать обработку ошибок REST API, IETF разработала RFC 7807 , который создает обобщенную схему обработки ошибок .
Эта схема состоит из пяти частей:
type
— идентификатор URI, который классифицирует ошибкузаголовок
— краткое, удобочитаемое сообщение об ошибкеstatus
– код ответа HTTP (необязательно)Detail
— понятное объяснение ошибки.экземпляр
— URI, который идентифицирует конкретное возникновение ошибки.
Вместо использования собственного тела ответа об ошибке мы можем преобразовать наше тело:
{
"type": "/errors/incorrect-user-pass",
"title": "Incorrect username or password.",
"status": 401,
"detail": "Authentication failed due to incorrect username or password.",
"instance": "/login/log/abc123"
}
Обратите внимание, что поле type
классифицирует тип ошибки, а instance
идентифицирует конкретное возникновение ошибки аналогично классам и объектам соответственно.
Используя URI, клиенты могут следовать этим путям, чтобы найти дополнительную информацию об ошибке , точно так же, как ссылки HATEOAS можно использовать для навигации по REST API.
Придерживаться RFC 7807 необязательно, но это выгодно, если требуется единообразие.
4. Примеры
Вышеупомянутые методы распространены в некоторых из самых популярных REST API. Хотя конкретные имена полей или форматов могут различаться на разных сайтах, общие шаблоны практически универсальны.
4.1. Твиттер
Давайте отправим запрос GET без предоставления необходимых данных аутентификации:
curl -X GET https://api.twitter.com/1.1/statuses/update.json?include_entities=true
API Twitter отвечает ошибкой с этим телом:
{
"errors": [
{
"code":215,
"message":"Bad Authentication data."
}
]
}
Этот ответ включает список, содержащий одну ошибку с ее кодом ошибки и сообщением. В случае с Twitter подробное сообщение отсутствует, и для обозначения того, что аутентификация не удалась, используется общая ошибка, а не более конкретная ошибка 401.
Иногда проще реализовать более общий код состояния , как мы увидим в нашем примере Spring ниже. Это позволяет разработчикам перехватывать группы исключений и не различать код состояния, который должен быть возвращен. Однако по возможности следует использовать наиболее конкретный код состояния.
4.2. Фейсбук
Подобно Twitter, Facebook Graph REST API также включает подробную информацию в свои ответы.
Давайте выполним запрос POST для аутентификации с помощью Facebook Graph API:
curl -X GET https://graph.facebook.com/oauth/access_token?client_id=foo&client_secret=bar&grant_type=baz
Мы получаем эту ошибку:
{
"error": {
"message": "Missing redirect_uri parameter.",
"type": "OAuthException",
"code": 191,
"fbtrace_id": "AWswcVwbcqfgrSgjG80MtqJ"
}
}
Как и Twitter, Facebook также использует общую ошибку — вместо более конкретной ошибки уровня 400 — для обозначения сбоя. В дополнение к сообщению и числовому коду Facebook также включает поле типа
, которое классифицирует ошибку, и идентификатор трассировки ( fbtrace_id
), который действует как внутренний идентификатор службы поддержки .
5. Вывод
В этой статье мы рассмотрели некоторые из лучших практик обработки ошибок REST API:
- Предоставление конкретных кодов состояния
- Включение дополнительной информации в органы реагирования
- Единая обработка исключений
Хотя детали обработки ошибок зависят от приложения, эти общие принципы применимы почти ко всем REST API, и их следует соблюдать, когда это возможно.
Это не только позволяет клиентам последовательно обрабатывать ошибки, но также упрощает код, который мы создаем при реализации REST API.
Код, указанный в этой статье, доступен на GitHub .