June 6

Как правильно оформлять коды ошибок в 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 без внутренних деталей и стек-трейсов.
  • Делать единый формат ошибки во всех эндпоинтах.
  • Если есть валидация, возвращать массив ошибок с описанием всех проблем, а не по одной.

И ключевое

Помните, что плохо спроектированные ответы об ошибках могут усложнить работу вам и вашей команде. Из-за этого можно потратить много часов на дебаг и разбор обращений от поддержки. При этом, если в команде или компании уже есть принятый формат, не стоит его ломать. Лучше сначала разобраться, почему он устроен именно так и какие ограничения за этим стоят.