Комментарии 40
Если почитать апологетов REST, то для кодов ошибок надо использовать HTTP статусы, а текст ошибки отдавать в теле или в специальном заголовке.
Не нужно читать апологетов, нужно читать первоисточники. REST ничего не говорит о том, каким образом использовать HTTP-коды (вообще, редкие упоминания HTTP в REST используются только в качестве примеров).
Если говорить об API, то я полностью согласен с автором. Из общих соображений ясно, что стандартный (и даже расширенный по WebDAV) набор HTTP-кодов никогда не покроет ошибки предметной области. Но вместо того, чтобы косплеить HTTP с его числовыми кодами, я бы предпочитал использовать уникальное имя ошибки. Такой подход удобно маппится на классы исключений в Python:
{
"error": "SMSGatewayError",
"message": "СМС-шлюз не отвечает, попробуйте позже"
}
HTTP 400 PUT /v1/task/1 { status: 'doing' }
Body: { error_code: '12', error_message: 'Задача уже взята другим исполнителем' }
Это очень странный подход использовать 400 ошибку в данном случае, 400 — это общая ошибка бизнес-логики. Здесь отлично подходит например HTTP/409.
В целом пространства 4хх ошибок вполне достаточно для четкого и понятного описания ошибок апи, а расширенные описания ошибок также ложатся в схему тела ответа при статусе 400 например
{
"code": "string",
"message": "string"
}
и даже позволяют делать неограниченный уровень вложенности:
{
"code": "authorization_failed",
"subError": {
"code": "payment_tool_rejected",
"subError": {
"code": "bank_card_rejected",
"subError": {
"code": "cvv_invalid"
}
}
}
}
Так что считаю проблему несколько надуманной (ну и для тех кто использует статус HTTP/200 при отдаче ошибки приготовлен специальный отдельный котел в аду).
За что я обожаю restfullapi, так это за то какое время тратится на меня г том какой статус и где подходит лучше всего.
Все-таки реко орые статусы кажется неплохо и оставить. Например 401, 403 потому что их зачастую переносят на сторону веб сервера или других промежуточных серверов.
400 — это общая ошибка бизнес-логики
400 означает синтаксически битый запрос, грубую ошибку клиента. Например, прислали XML вместо JSON или вообще сломали заголовки. Или тут что-то другое имелось ввиду?
Поэтому можно неделю потратить на обсуждение формата ошибок с коллегами споря о кошерности того или иного решения.
Я обычно советую не изобретать велосипед, а посмотреть что делают гиганты и взять что то среднее.
Например фейсбук использует
{
"error": {
"message": "Message describing the error",
"type": "OAuthException",
"code": 190,
"error_subcode": 460,
"error_user_title": "A title",
"error_user_msg": "A message",
"fbtrace_id": "EJplcsCHuLu"
}
}
{
"errors": [
{
"parameter": "start_time",
"details": "invalid date",
"code": "INVALID_PARAMETER",
"value": "",
"message": "Expected time, got \"\" for start_time"
}
],
"request": {
"params": {
"account_id": "hkk5"
}
}
}
Ведь не от хорошей жизни они пришли к этому :)
/users — список пользователей
/users/123 — пользователь с ид= 123.
Чтобы соответствовать HTTP, применим правило — если в адресе эндпоинта какого-то объекта нет, то ошибка 404.
То есть:
/users — арес всегда правильный, возвращаем всегда список (или пустой список)
/users/123 — если пользователя 123 нет, то ошибка 404 иначе возвращаем объект.
Ну и понятно, что это утрировано, и дальше если обект есть но у него проблемы
то могут кидаться уже ошибки с другими кодами.
В REST всегда path-часть URL это адрес какого-то ресурса (сущности).
Ресурс адресуется по всему URI (кроме fragment), включая host, path и query, а не только по его path-части. Т.е. /users/42?v=1
и /users/42?v=2
могут адресовать два разных ресурса.
https://greenbytes.de/tech/webdav/rfc3986.html#query
А зачем вообще использовать 4ХХ рендж? почему не 500 + детали в теле? Это мне кажется более логичным. Хотя сам не использую HTTP коды для бизнес логики, те всегда 200 код если отработал скрипт и сформировал ответ, исключения: если надо клиенту указать определенные дествия предписанные HTTP протоколом, типа 301/302/401 и т.п.
2xx — успех
3xx — перенаправление
4xx — ошибка клиента
5xx — ошибка сервера
6хх — ошибки бизнеса
(кажется нам этого нехватает)
Про привычку — думаю это рационально. Как раз с переходом на gRPC может оправдать себя.
Не стоит использовать 5xx в штатных сценариях, т.к. часто на эти коды вешают какой нить высокоуровневый алертинг по умолчанию. Ops-ы вас проклянут за такое.
От комбинаторного взрыва начинает конкретно пухнуть башка!
Если на клиенте ответ с ошибкой (например все статусы >= 400) превратить в строку вида:
<status_code>.<error_name_from_body>
то комбинаторный взрыв пропадает и нет необходимости делать большую вложенность if-чиков. А если error_name_from_body
— это уникальная строка, то можно вообще на статус не смотреть, пока это не требуется.
И при этом сохраняется возможность простой группировки ошибок на основе статусов. Например на все статусы 5xx просто показывать пользователю текст из тела ошибки и больше ничего не делать. И для мониторинга как правило не придётся писать кастомную парсилку — хватит статусов.
Это началась DDOS атака, или кто-то из разработчиков выбрал неудачный статус?
У нас проблемы с выбором правильного статуса решается с помощью просвещения молодых разработчиков (в первую очередь посылаем их на страницу википедии с описанием всех статусов) и ревью пул-реквестов.
Какой статус надо вернуть в случае, если пользователь хочет взять задачу, а ее уже взял в работу другой пользователь?
Это сильно зависит от того, что технически означает "взять задачу" и как это будет реализовано в API. И скорее всего проблема с выбором статуса ошибки возникает из-за неподходящей для этой задачи реализации API. Вот пару примеров реализации и подходящего статуса:
- Все юзеры видят все задачи и могут сделать UPDATE задачи, которая ещё не занята (прописать в неё свой user_id). В таком случае можно запретить делать UPDATE задачи всем, кроме её "владельца" — статус ответа 403. PS: В целом это может быть кривая реализация, т.к. есть возможность передать чужой user_id и это надо дополнительно обрабатывать.
- У юзеров есть список "своих" задач. "Взять задачу" означает добавить задачу из общего списка в свой список, передав в запросе её ID. В данном случае ошибка — это ошибка валидации ID задачи (клиент передал ID занятой задачи). Статус для неё — 422.
- и т.п.
Если не делать "сову" из API поверх HTTP, то не придётся натягивать её на "глобус". Чаще всего проблемы возникают из-за недостаточно хорошего понимания принципов заложенных в HTTP и как его использовать в соответствии с задумкой его авторов. В моём опыте, при подходящей реализации API, не было случаев, что бы не получилось выбрать подходящий HTTP-статус для ошибки. А если такое и случалось, то это было верным признаком, что я выбрал не правильную реализацию и надо конкретный API сделать по другому.
И я не могу вспомнить, что бы в рамках одного ресурса один и тот же статус обозначал бы разные ситуации и реально требовалось бы смотреть на некий "error_name_from_body". Для разных ресурсов один и тот же статус мог иметь немного отличный смысл (и это логично — ресурсы ведь разные), но он всё равно оставался в рамках того для чего этот статус был предназначен в HTTP. Могу только представить, что статус 403 может потребовать дополнительного уточнения конкретной причины запрета доступа (надо войти под другим юзером или в принципе запрещён доступ для всех). И то это скорее всего редкий случай.
Идея вцелом хорошоая. Только я бы все же оставил несколько статусов которые на граинце приложения.транспорта. Это например статусы связанные с авторизацией и аутентификацией 401, 403, 419, 429. И разделил отдельно выделил уровень ошибок валидации входных параметров (наличие обязаьельных полей, их тип и формат, вохождение в диапазоны допутимых значени) то что как бы дублирует работу валидацию которая на фронтенде должна проходить, но мы часто ее дулируем для верности на бэкенде. Мотивация такая что эта валидация дет иметь достаточно однообразный для всех случаев тип: список полей, текст с пояснением ошибки кторый возможно рна фронтенде будет поазан радом с ошибочным полем. В то же время ошибка например что у клиента недостаточно денег на счете если сама сумма входит в заданные рамки это уже ошибка приложения, или номкер телефона или почта прошли формальну валидацию при регистрации но клиент с такими значениями уже зарегистирован в системе.
Придумали стандарт и было бы неплохо им пользоваться. По сути автор пришел к чему-то похожему с общим гарантированным форматом ошибки.
Я бы рекомендовала использовать и обрабатывать коды следующим образом:
2хх — ответ успешный, читаем ожидаемый ответ
4хх — ответ не успешный, и сервис гарантирует, что ретраи будут возвращать тоже самое:
ошибки валидации, ошибки доступа, ошибки роутинга
5хх — ответ не успешный, но клиенту возможно имеет смысл ретрайнуть запрос пару раз через некоторое время: отвалилась бд, недоступен зависимый сервис етк
Как видите:
1. Нет привязки к конкретным цифрам кода кроме последней.
2. Ошибка скорее всего в теле будет содержать подробности, описанные форматом по ссылке выше. Можно матчить по типу и обрабатывать (если хочется), а можно просто показать пользователю Details.
Есть же стандарт RFC 7807 там достаточно ясно описано как нужно возвращать ошибки.
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json
Content-Language: en
{
"type": "https://www.thecodebuzz.com/probs/account-balance-low",
"title": "You do not have enough balance.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/checking-account/msgs/bd",
"balance": 30, //Add your custom fields
"accounts": ["/account/checking-account/0012"] // add your custom fields
}
Как работать с ошибками бизнес-логики через HTTP