05 февр. 2026 г.·7 мин чтения

Формат API-ответов с ошибками, который команды могут использовать в любом клиенте

Используйте формат API-ответов с ошибками с понятными сообщениями, машинными кодами и подсказками по повтору, чтобы приложения, support и product одинаково читали сбои.

Формат API-ответов с ошибками, который команды могут использовать в любом клиенте

Почему команды по-разному понимают API-сбои

Один неудачный запрос часто превращается в три разные истории. В web-приложении пользователь видит «Что-то пошло не так». На mobile ему показывают сообщение о тайм-ауте. В admin-инструменте support видит простое «500» без контекста. Backend ошибся один раз, а каждый клиент описывает это по-своему.

Из-за этого очень быстро появляется лишняя работа. Support не может понять, пользователь ввёл неверные данные, потерял доступ или столкнулся с проблемой на сервере. Они просят engineering найти логи. Engineering просит request ID или скриншот. Пользователь ждёт, пока все пытаются угадать, что случилось.

Команды product тоже получают искажённые сигналы. Если одна проблема с авторизацией выглядит как «login failed» в mobile, «session expired» в web и «unknown error» во внутреннем инструменте, статистика тикетов становится шумной. Команда может решить, что у них три разные проблемы, хотя на самом деле их только одна. Это замедляет решения и часто ведёт к неправильному исправлению.

Разные клиенты усугубляют ситуацию, потому что каждый из них старается решать свои задачи. Web-командам нужен дружелюбный текст. Mobile-командам нужен короткий текст, который помещается на маленьком экране. Внутренние инструменты часто показывают сырые status code, потому что их проще быстрее выпустить. Сами по себе все эти решения не плохие. Проблема начинается тогда, когда для одного и того же сбоя каждый придумывает свою форму.

В итоге теряется простая вещь:

  • что именно сломалось
  • кто может это исправить
  • что должно произойти дальше

Единый формат API-ответа с ошибкой даёт всем клиентам один и тот же базовый набор фактов. Пользователь по-прежнему видит понятный текст, support может искать по машинному коду, product — группировать одну и ту же проблему в разных приложениях, а клиент — решать, нужно ли повторить запрос, попросить новые данные или остановиться и показать понятное сообщение.

Команды обычно замечают ценность после первого сложного инцидента. Достаточно одного сбоя в оплате или сломанного входа в систему. Если форма ошибки единая, люди перестают спорить о симптомах и начинают исправлять причину.

Что должен включать каждый ответ с ошибкой

Хороший payload ошибки быстро отвечает на два вопроса: что пошло не так и что делать дальше. Если хотя бы один из ответов отсутствует, код приложения усложняется, а support начинает гадать.

Начните с короткого сообщения для человека. Пишите просто. «Email address is invalid» звучит лучше, чем «Validation failed for input payload». Сообщение должно помогать тому, кто пользуется приложением, читает логи или смотрит обращение в support.

Одного человеческого сообщения недостаточно. Клиентам нужен ещё и стабильный код, который не меняется, когда вы переписываете текст, переводите его или делаете его дружелюбнее. Mobile-приложение может ориентироваться на invalid_email для поведения формы. Support-инструмент может группировать один и тот же сбой по множеству тикетов. Вот где хорошее проектирование кодов ошибок окупается.

HTTP status по-прежнему важен, но он не может нести весь смысл. 400 говорит только о том, что запрос плохой. Он не объясняет, был ли пароль слишком коротким, отсутствует ли поле или значение нарушило бизнес-правило. Укажите status в ответе, а рядом добавьте детали, не пряча их за ним.

Подсказки для повторной попытки экономят много времени. Некоторые сбои нельзя повторять ни в коем случае, например плохой API token. Другие — можно, например timeout или временный rate limit. Сделайте это явным с помощью простого флага и, если возможно, добавьте задержку.

Практичная форма ошибки обычно включает:

  • message для человека
  • code для логики клиента и отчётности
  • status для HTTP-результата
  • retryable и, если нужно, retry_after_seconds
  • место для field_errors, request_id и короткой docs_note

Последняя группа важнее, чем ожидают команды. Field errors позволяют форме подсветить точное поле, в котором есть ошибка. Request ID даёт support и engineering один общий ключ для логов. Docs note может добавить короткую подсказку вроде «Use an access token with write scope» без лишних догадок.

Если держать эти части единообразными во всех endpoints, web, mobile, backend jobs и support будут читать одну и ту же историю из одного и того же ответа. Уже этого достаточно, чтобы заметно сократить число избегаемых тикетов.

Как собрать формат шаг за шагом

Хороший формат API-ответа с ошибкой начинается с малого. Если сразу добавить десять полей, каждый клиент будет использовать свои пять, и путаница начнётся с первого дня. Большинству команд хватает короткой базовой структуры, которой можно доверять.

Используйте одни и те же несколько полей везде:

  • code для стабильного машинного имени
  • message для простого языка
  • retryable для ответа «да» или «нет»
  • request_id, чтобы support мог найти неудачный запрос
  • details только тогда, когда клиенту нужен дополнительный контекст

Делайте имена скучными и стабильными. Звучит не так эффектно, но здесь скучность выигрывает. Не переименовывайте code в error_code позже только потому, что так нравится одной команде. Когда web, mobile, backend-логи и support-инструменты зависят от имени поля, его изменение обходится дороже, чем кажется.

Напишите один JSON-пример заранее, до того как распространите формат по сервисам. Потом проверьте именно этот пример везде, где его будут читать. Браузерное приложение может показывать message. iOS-приложение может использовать code, чтобы подобрать локальный текст. Support-инструменту первым может понадобиться request_id. Если у одного клиента возникают проблемы со структурой, исправьте контракт сейчас, а не обходите проблему позже.

Также нужно чётко разделять данные для клиента и внутренние данные. Пользователь может видеть короткое сообщение вроде «Payment failed. Try another card.». Он не должен видеть stack trace, сырые ошибки провайдера или названия баз данных. Храните внутренние поля отдельно или не включайте их в ответ и помещайте в логи.

За изменения формата должна отвечать небольшая команда. Если никто не владеет этим, каждый сервис начнёт расходиться в свою сторону. Назначьте одного человека или небольшую backend-группу, напишите короткое правило для добавлений и относитесь к изменениям как к API-изменениям, а не как к мелкой правке.

Если хотите простой тест, дайте один и тот же пример ошибки frontend-разработчику, mobile-разработчику и сотруднику support. Если все трое понимают, что именно сломалось и что будет дальше, формат выполняет свою задачу.

Пишите сообщения, с которыми можно что-то сделать

Хороший формат API-ответа с ошибкой начинается с сообщения, которое реальный человек поймёт с первого прочтения. «Validation failed» слишком расплывчато. «We couldn't create your account because the email address is already in use» даёт пользователю и support понятную точку опоры.

Простой язык важнее красивых формулировок. Назовите действие, которое не удалось, назовите причину, если она известна, и остановитесь на этом. Если пользователь может исправить проблему, скажите как. Если не может — скажите, что будет дальше.

Хорошо работает простой шаблон: что произошло, а потом что делать сейчас. Например: «We couldn't save your changes because your session expired. Sign in again and try once more.» Это сообщение легко показать в web-приложении, mobile-приложении или support-чате без переписывания.

Держите текст для пользователя чистым. Не добавляйте в сообщение stack trace, SQL errors, имена вендоров или внутренние детали сервисов. «Stripe card_error» или «PostgreSQL unique constraint violation» помогает инженерам в логах, а не покупателям на экране оплаты.

Короткие сообщения обычно работают лучше всего:

  • «This password reset link has expired. Request a new link and try again.»
  • «We couldn't upload the file because it is larger than 10 MB. Choose a smaller file.»
  • «You do not have permission to view this project. Ask the project owner for access.»
  • «We couldn't process the request right now. Try again in a few minutes.»

Тон должен быть одинаковым во всех endpoints. Если один API говорит «Auth failed», а другой — «Your sign-in session expired», пользователи думают, что это разные проблемы, хотя это не так. Support чувствует эту боль первым. Им приходится вручную переводить каждое сообщение на нормальный язык.

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

Если у вас небольшая продуктовая команда, проверяйте текст ошибок так же, как проверяете подписи кнопок. Попросите одного человека из support и одного из product прочитать сообщение без дополнительного контекста. Если оба могут ответить на вопросы «что произошло» и «что теперь должен сделать пользователь», сообщение, скорее всего, готово.

Добавьте машинные коды и подсказки для повторов

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

Надёжный формат API-ответа с ошибкой не просто сообщает о сбое. Он подсказывает и софту, и людям, что произошло, что делать дальше и имеет ли смысл новая попытка. Без этого каждая команда клиента строит собственную догадку, а support потом разбирает беспорядок.

Делайте машинные коды короткими и понятными. AUTH_EXPIRED, RATE_LIMITED и INVALID_FIELD легко читать в логах, дашбордах и заметках к тикетам. Можно группировать коды с помощью простого префикса вроде AUTH_ или INPUT_, но каждый код должен означать только одну проблему. Если AUTH_FAILED в одном месте означает «bad password», а в другом — «token expired», доверять ему уже нельзя.

Подсказки для повторной попытки требуют той же дисциплины. Добавляйте их только тогда, когда следующая попытка действительно может сработать. Timeout или кратковременная перегрузка сервиса могут пройти сами. Истёкший token нужно сначала обновить. Сломанное тело запроса не исправится само, сколько бы раз приложение ни пыталось его отправить.

Небольшая структура ответа покрывает большинство случаев:

{
  "code": "RATE_LIMITED",
  "message": "Too many requests. Try again in 30 seconds.",
  "retryable": true,
  "retry_after_seconds": 30,
  "action": "wait"
}

Поле action убирает догадки. Используйте простые значения вроде wait, refresh_auth или stop. Web, mobile, backend и support-команды могут следовать одному и тому же правилу. Когда клиент спрашивает: «Стоит ли попробовать ещё раз?», ответ уже есть в response.

Разделяйте временные сбои и жёсткие остановки. Временные сбои включают rate limits, timeout и краткие простои. Жёсткие остановки включают неверный input, отсутствие прав или ресурс, которого больше не существует. Если смешать их вместе, приложения будут продолжать повторять запрос там, где нужно остановиться, а пользователи будут видеть один и тот же сбой снова и снова.

Несколько правил помогают держать всё в порядке:

  • Используйте короткие, читаемые коды.
  • Присваивайте каждому коду только одно значение.
  • Помечайте ошибки как retryable только тогда, когда повтор может помочь.
  • Добавляйте время ожидания для случаев с backoff.
  • Говорите клиентам, когда нужно обновить auth или прекратить повторы.

Такой проектирование кодов ошибок выглядит небольшим, но экономит реальное время. Один понятный код может сократить переписку support, уменьшить шум в логах и сохранить единообразие во всех клиентах.

Простой пример для web, mobile и support

Во время checkout не проходит оплата картой. Покупатель видит spinner, нажимает «Pay again», а потом открывает чат, потому что ничто не объясняет, что произошло. Хороший формат API-ответа с ошибкой делает этот момент намного яснее.

{
  "error": {
    "code": "PAYMENT_AUTH_FAILED",
    "message": "The bank declined this payment.",
    "user_hint": "Try another card or contact your bank.",
    "retryable": false,
    "request_id": "req_9f3c2a"
  }
}

На странице оплаты в web приложение может показать «Payment declined. Try another card.». В mobile тот же код можно превратить в «Your card was declined. Use a different card or check with your bank.». Формулировка меняется под экран, но код остаётся тем же.

Одна эта деталь сразу помогает support. Если клиент говорит «checkout is broken», сотрудник может попросить request ID, найти req_9f3c2a и увидеть PAYMENT_AUTH_FAILED. Теперь понятно, что API ответил, банк отклонил платёж, и клиенту не стоит снова пробовать ту же карту.

Подсказка для повторной попытки меняет и поведение приложения. Когда retryable равно false, web и mobile должны остановить автоматические повторы и показать следующий шаг. Когда код вроде PAYMENT_GATEWAY_TIMEOUT, а в ответе указано retryable: true, приложение может один или два раза повторить запрос в фоне, прежде чем попросит клиента попробовать снова.

Единая форма держит каждую команду в своей зоне ответственности:

  • Клиенты получают понятный текст, с которым можно работать
  • Приложения читают код и подсказку по повтору вместо догадок
  • Support использует код и request ID, чтобы найти точный сбой
  • Product получает одно понятное название в отчётах вместо пяти расплывчатых

Без этого web может назвать проблему network error, mobile показать общий failure, а support — обвинить не ту систему. С одной формой каждый клиент рассказывает одну и ту же историю, даже если текст на экране отличается.

Ошибки, которые создают больше тикетов

Исправьте логику повторов
Разберите, где клиентам нужно повторить запрос, подождать, обновить авторизацию или остановиться.

Слабый формат API-ответов с ошибкой быстро создаёт работу для support. Когда API возвращает только текст вроде «Something went wrong», приложение не может нормально среагировать, support не может сортировать случаи, а команды product начинают гадать.

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

Ещё одна частая путаница возникает, когда один код покрывает несколько несвязанных сбоев. Если PAYMENT_FAILED означает «card expired», «bank declined» и «provider timeout», каждому клиенту приходится разбирать текст сообщения и делать предположение. Обычно это приводит к неправильной кнопке, ошибочному повтору или тикету, которого вообще не должно было быть.

Подсказки для повторной попытки создают ещё больше путаницы, когда команды используют их слишком свободно. Если пользователь ввёл неверный email, выбрал неправильную дату или пропустил обязательное поле, клиент не должен предлагать повтор. Пользователю нужно исправить данные. Ложная подсказка о повторе только тратит время и приучает людей нажимать ту же кнопку снова и снова.

Сырые внутренние ошибки — ещё одна фабрика тикетов. Пользователи никогда не должны видеть database exception, stack trace или сообщение провайдера, скопированное напрямую от стороннего сервиса. Эти детали помогают инженерам в логах, а не клиентам в приложении. Давайте пользователям понятное сообщение простым языком, а внутреннюю причину оставляйте в логировании или tracing.

Изменения старых полей могут вредить месяцами, особенно на mobile. Если переименовать code, перенести message в другой объект или убрать retryable без плана версионирования, старые клиенты продолжат ломаться ещё долго после того, как backend уже обновится. Сначала добавляйте новые поля, какое-то время поддерживайте оба формата, а старые удаляйте только после того, как клиенты подтянутся.

Небольшой пример показывает разницу. Если сервер возвращает INVALID_PHONE, retryable: false и короткое сообщение вроде «Enter a valid phone number», web-приложение может подсветить поле, mobile-приложение — оставить форму открытой, а support — заметить проблему одним взглядом. Такая единообразность сокращает число тикетов сильнее, чем любая красивой видимости status page.

Быстрая проверка перед запуском

Сократите шум в тикетах
Превратите разрозненные сообщения в понятные шаблоны ошибок, с которыми может работать команда.

Чистый формат API-ответов с ошибкой всё равно может сломаться в production, если одному клиенту достаётся меньше деталей, чем другому. Перед релизом проверьте типичные сбои: неверный input, истёкший login, rate limits, timeout и конфликты. Именно эти случаи создают большую часть путаницы.

Проведите короткую проверку релиза на каждом клиенте и endpoint:

  • Убедитесь, что каждая ошибка включает и человеческое сообщение, и машинный код.
  • Убедитесь, что подсказки по повтору совпадают с реальным поведением сервера.
  • Убедитесь, что формы возвращают field-level errors, а не только одну общую ошибку.
  • Убедитесь, что логи, дашборды и support-инструменты показывают один и тот же request или error ID.
  • Убедитесь, что старые клиенты продолжают работать до того, как вы переименуете или удалите поля.

Первый пункт кажется очевидным, но команды постоянно его пропускают. Сообщение помогает человеку, который пользуется приложением. Код помогает приложению решить, что делать дальше. Если один endpoint возвращает только «Request failed», support не сможет понять, должен ли пользователь повторить запрос, ввести данные заново или обратиться к кому-то.

Правила повторов нужно тестировать по-настоящему. Если ваш API говорит «try again later», а сервер продолжает отклонять тот же запрос в течение десяти минут, доверие пользователей быстро падает. Rate limits, временные проблемы у upstream-сервиса и очереди задач часто ведут себя не так, как ожидается. Проверяйте их с реальными задержками и повторными запросами.

Формы заслуживают отдельного внимания. Одной общей ошибки обычно недостаточно, если неверны сразу три поля. Если формат email плохой, а пароль слишком короткий, верните обе field errors, чтобы приложение могло указать на нужные поля. Это сильно сокращает переписку на mobile, где общие предупреждения особенно раздражают.

Общие ID тоже важны. Если API возвращает error ID, ваши логи и дашборды должны сохранять тот же самый ID. Тогда support сможет искать одно значение и находить весь след. Без этого product, engineering и support будут по-разному рассказывать об одном и том же сбое.

Одна последняя ловушка бьёт по старым приложениям. Переименование с "error_code" на "code" выглядит мелочью, но старая mobile-сборка может перестать читать ошибки вовсе. Сохраняйте тесты совместимости для старых клиентов, особенно если пользователи обновляются не сразу.

Что делать дальше

Начните с малого. Если пытаться менять все endpoints одновременно, работа обычно превращается в длинный спор о крайних случаях и названиях.

Выберите одну общую схему и сначала проверьте её на двух часто используемых endpoints. Хорошие кандидаты — login и payment или любые два сценария, которые часто вызывают вопросы в support. Так product, support и engineering будут обсуждать что-то реальное, а не спорить о черновике в документе.

Практичный план внедрения выглядит так:

  • Выберите одну форму ответа для всех клиентов: человеческое сообщение, стабильный машинный код и понятную подсказку по повтору.
  • Примените её к двум endpoints, которые уже хорошо знакомы людям.
  • Пересмотрите старые ошибки и сопоставьте их с новым набором кодов, даже если часть старых сообщений ещё какое-то время будет неидеальной.
  • Напишите короткую заметку для команды с несколькими примерами и простыми правилами повторов вроде «try later», «do not retry» или «ask the user to fix input».

Эта заметка для команды важнее, чем многие ожидают. Короткая страница может сильно уменьшить путаницу, если на ней есть точный payload, объяснение смысла каждого кода и указание, кто отвечает за исправление. Support нужно знать, что говорить клиенту. Product нужно понимать, какие ошибки указывают на плохие данные, а какие — на сломанный сценарий. Engineers нужно знать, когда клиент должен повторить запрос, а когда ему нужно остановиться.

Сделайте ещё одну быструю чистку старых ответов. Ищите дублирующиеся коды, расплывчатые сообщения вроде «something went wrong» и случаи, когда сервер возвращает один и тот же текст для трёх разных сбоев. Именно они чаще всего тратят больше всего времени.

Если вашей команде нужен внешний взгляд, Oleg Sotnikov может посмотреть на ваш формат API-ответов с ошибкой и предложить модель, которой смогут пользоваться product, support и engineering. Такой обзор особенно полезен до того, как формат распространится на mobile, web и внутренние инструменты, когда маленькие исправления ещё стоят недорого.