Как правильно оформлять коды ошибок в API
Если мы говорим про проектирование, то аналитики часто продумывают формат запроса и ответа API, но забывают о том, как должны выглядеть ошибки. Часто это происходит потому, что в компании уже принят какой-то стандарт, и все привыкают работать по накатанной. Такой подход хорошо работает в устоявшихся проектах, но если у вас новый проект, лучше самостоятельно продумать ответ на случай, если что-то пойдёт не так. Ведь ошибки — это часть контракта, и если вы не дадите конкретики, разработчики сделают так, как им кажется правильным.
Из чего состоит хорошая ошибка?
Любая ошибка должна отвечать на три вопроса: что произошло, почему и что с этим делать.
Что произошло?
Ответ на этот вопрос обычно закрывают коды ответов 4xx и 5xx.
Например:
400 Bad Request — некорректные данные от клиента;
401 Unauthorized — пользователь не аутентифицирован (нет токена или он недействителен);
403 Forbidden — аутентификация есть, но нет прав доступа;
404 Not Found — ресурс не найден;
409 Conflict — конфликт состояния (например, такой email уже зарегистрирован);
422 Unprocessable Entity — данные приняты, но не прошли бизнес-валидацию;
429 Too Many Requests — превышен лимит запросов;
500 Internal Server Error — ошибка на стороне сервера;
503 Service Unavailable — сервис временно недоступен.
Но достаточно ли этого? В некоторых кейсах да, но иногда лучше вместе с кодом отправлять в теле ответа мнемонику, например: USER_NOT_FOUND, VALIDATION_ERROR, DUPLICATE_ORDER. Если это указать, клиенту будет проще разобраться и сделать обработку более персонализированной
Почему это произошло?
Частая ситуация: клиент получает ошибку, но, чтобы понять реальную причину, приходится лезть в логи и вручную разбираться. Поэтому лучше сразу дать пояснение. Для этого в ответ, помимо мнемоники, добавляют поле message с понятным описанием проблемы. В него не стоит включать технические трейсы или детали, которые не будут понятны пользователю.
Что с этим делать?
В идеале — добавить инструкцию по исправлению ошибки. Например: «повторить запрос через N секунд», «исправить данные», «обратиться в поддержку». Иногда даже передают номер телефона или другие контакты поддержки.
В теории всё звучит хорошо. Теперь давай разберёмся, как это реализовать и как может выглядеть такой ответ.
Структура тела ошибки
Если ошибки во всех эндпоинтах выглядят одинаково, клиентам проще писать обработчики. Именно поэтому лучше использовать единый формат.
Хороший базовый формат тела ошибки:
{
"code": "VALIDATION_ERROR",
"message": "Ошибка валидации",
"errors": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Неправильный формат email"
}
]
}code — машиночитаемая мнемоника, по которой клиент понимает, что делать.
message — человекочитаемое описание ошибки.
errors — массив, позволяющий вернуть все проблемы валидации за один запрос, а не по одной. Это особенно важно для форм, но лучше, чтобы контракт был единым для всех эндпоинтов.
Иногда вместо errors, если API простое и не требует детальной валидации, добавляют поле details или messageDetails, чтобы описать, как исправить ситуацию:
{
"code": "DUPLICATE_ORDER",
"message": "Заказ с id 12345 уже существует",
"messageDetails": "Заказ с номером 12345 уже существует. Если это ошибка, напишите в чат поддержки"
}Еще один хороший паттерн — добавлять идентификатор запроса, чтобы можно было отследить всю цепочку вызовов. Обычно его передают в поле traceId или correlationId.
{
"code": "DUPLICATE_ORDER",
"message": "Заказ с id 12345 уже существует",
"traceId": "abc-123-xyz"
}Частые антипаттерны, которые я встречаю на практике
1)Отвечать HTTP-кодом 200 на любые ответы, а реальный код ошибки передавать в теле ответа. Например:
{
"code": "404",
"message": "NOT FOUND"
}2) Использовать разные форматы ошибок в разных эндпоинтах. В одном месте {"error": "..."}, в другом {"message": "..."}. В этом случае клиенту или сервису, который с вами интегрируется, придётся делать отдельный парсер для каждого варианта ошибки и затем унифицировать их.
3) Передавать стандартные сообщения вроде «что-то пошло не так» или технические ошибки типа NullPointerException в message. Такой формат не даёт никакой полезной информации.
Что фиксировать в требованиях при проектировании API
- HTTP-статус должен соответствовать типу ошибки, а не всегда быть 200.
- Указывать поле code с машиночитаемой мнемоникой.
- Добавлять поле message без внутренних деталей и стек-трейсов.
- Делать единый формат ошибки во всех эндпоинтах.
- Если есть валидация, возвращать массив ошибок с описанием всех проблем, а не по одной.
И ключевое
Помните, что плохо спроектированные ответы об ошибках могут усложнить работу вам и вашей команде. Из-за этого можно потратить много часов на дебаг и разбор обращений от поддержки. При этом, если в команде или компании уже есть принятый формат, не стоит его ломать. Лучше сначала разобраться, почему он устроен именно так и какие ограничения за этим стоят.