$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогHTTP контракт для ошибок: почему статус — это не единственное поле

HTTP контракт для ошибок: почему статус — это не единственное поле

$ sudo teach IT
·25 июля 2026 г.·9 мин·47
HTTP контракт для ошибок: почему статус — это не единственное поле

На примерах покажем, как связать HTTP-статусы, машинные коды ошибок, детализацию для отладки и безопасные сообщения. Разберём случаи 4xx/5xx и как договориться со сторонними клиентами.

Содержание
Что такое “HTTP контракт для ошибок”Почему одного статуса малоБазовый формат ошибки: поля, которые реально работаютМинимальный набор vs расширенныйКак связать HTTP-стататусы и коды ошибокТаблица: типовые сочетания 4xx/5xx и кодыРекомендация по стабильностиДетализация для отладки vs безопасные сообщенияДва уровня сообщений: user-facing и debug-facingЧто нельзя отправлять в detailsБаланс: поле за полемПримеры контрактов для распространённых сценариев1) Невалидный JSON: 400 + ERRINVALIDJSON2) Валидация домена: 422 + ERRVALIDATIONFAILED3) Нет прав: 403 + ERRPERMISSIONDENIED4) Конфликт состояния: 409 + ERRCONFLICT5) Ошибка downstream: 503 + ERRDEPENDENCYUNAVAILABLEКак действовать с 4xx и 5xx: ретраи, идемпотентность и “правильные” статусыЧто обычно ретраитьЧто обычно НЕ ретраитьИдемпотентность и 409Как договориться со сторонними клиентами и не сломать интеграции1) Версионируйте контракт ошибок2) Правило совместимости: добавляй, но не переименовывай3) Document your codes

HTTP-статус сам по себе — удобный и привычный сигнал. Но на практике он редко бывает достаточным: одному и тому же статусу соответствуют десятки разных причин, каждая из которых требует разной реакции клиента, разной логики ретраев и разных мер безопасности. Если вы ограничиваетесь только статусом, вы почти наверняка столкнётесь с ситуациями вроде: клиент “видит 400”, но не понимает, что именно сломалось; поддержка получает логи без контекста; интеграции становятся хрупкими; а ошибки, предназначенные только для диагностики, утекут наружу.

Хороший подход — рассматривать ошибки как контракт между сервером и клиентом: набор полей и правил, которые позволяют машине и человеку принять правильное решение. В этом материале разберём, как связать HTTP-статусы, машинные коды ошибок, детализацию для отладки и безопасные сообщения. Поговорим про типовые сценарии 4xx/5xx, и отдельно — как договариваться со сторонними клиентами, чтобы не “сломать” интеграцию следующей версией API.


Что такое “HTTP контракт для ошибок”

Контракт — это не только схема ответа. Это ещё и правила:

  • какие статусы вы используете и за какие нарушения;
  • какие машинные коды ошибок (например, ERR_INVALID_EMAIL) идут вместе с ними;
  • какие поля предназначены для автоматической обработки клиентами;
  • какие поля безопасны для отображения пользователю;
  • как вы включаете детали для отладки, но не раскрываете внутренности;
  • как соотносятся логи сервера и ответы клиента (корреляция);
  • что меняется в контракте совместимо, а что — только с версионированием.

Почему одного статуса мало

HTTP-статус — это уровень семантики “в целом”:

  • 400 Bad Request — “запрос некорректный”
  • 401 Unauthorized — “не аутентифицирован”
  • 403 Forbidden — “нет прав”
  • 404 Not Found — “не найдено”
  • 409 Conflict — “конфликт”
  • 422 Unprocessable Entity — “синтаксис ок, но смысл не прошёл валидацию”
  • 500 Internal Server Error — “что-то пошло не так”
  • 503 Service Unavailable — “временно недоступно”

Но клиенту часто нужна конкретика: ошибка в поле email, ограничение по длине, истёк токен, конфликт идемпотентности, недоступен downstream-сервис, ошибка сериализации, сбой валидации бизнес-правил и т.д.

Без машинного кода клиент вынужден:

  1. парсить текст сообщения (что плохо и ломается при локализации);
  2. гадать по статусу и контексту;
  3. держать много эвристик “если статус 400 и путь /users — значит...”.

В итоге растёт стоимость поддержки интеграций.


Базовый формат ошибки: поля, которые реально работают

Начнём с практичного шаблона. Для большинства API удобна структура, похожая на RFC-подобные “проблемы” (в духе Problem Details for HTTP APIs, но можно адаптировать под себя).

Пример ответа для ошибок:

code
{
  "error": {
    "type": "https://api.example.com/errors/invalid-request",
    "code": "ERR_INVALID_REQUEST",
    "message": "Проверьте поля запроса и повторите попытку.",
    "details": {
      "fieldErrors": [
        { "field": "email", "message": "Неверный формат" },
        { "field": "age", "message": "Значение должно быть >= 18" }
      ]
    },
    "correlationId": "b7f2b7a0-8f7f-4a1b-9c5f-3e2f0b4a6b31"
  }
}

Разберём поля:

  • code — машинный стабильный идентификатор ошибки. Его лучше использовать в бизнес-логике клиента.
  • message — безопасное человекочитаемое пояснение. Оно не должно раскрывать внутренности (SQL, stack trace, имена таблиц).
  • details — дополнительные сведения для отладки/интерактивного UX. Может содержать “поле → причина”, но границы безопасности важны.
  • correlationId — идентификатор, который клиент возвращает обратно (в саппорт или в повторных запросах), а сервер использует для поиска в логах.
  • type (опционально) — ссылка на документацию ошибки или категоризацию. Хорошо, если клиентам нужно понимать “тип проблем” стандартизировано.

Минимальный набор vs расширенный

На зрелых API обычно есть два слоя:

  1. минимальный (стабильный, для программной обработки): code, message (или userMessage), возможно correlationId;
  2. расширенный (для диагностики): details, список полей, downstream-идентификаторы, ссылки на примеры.

Разделяйте их по аудитории: всё, что можно приложить клиенту без риска, уходит в ответ. Всё, что нельзя — остаётся в логах, но “подсвечивается” корреляцией.


Как связать HTTP-стататусы и коды ошибок

Ключевая идея: статус отвечает транспортной/протокольной семантике, а код описывает доменную причину.

Таблица: типовые сочетания 4xx/5xx и коды

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

4xx: ошибки клиента

  • 400 Bad Request
    • ERR_INVALID_JSON — невалидный JSON / неверная кодировка
    • ERR_INVALID_SYNTAX — нарушена структура (например, отсутствует обязательный блок)
  • 401 Unauthorized
    • ERR_AUTH_MISSING — нет заголовка аутентификации
    • ERR_AUTH_EXPIRED — токен истёк
    • ERR_AUTH_INVALID — токен подпорчен/не проходит валидацию
  • 403 Forbidden
    • ERR_PERMISSION_DENIED — нет прав
    • ERR_SCOPE_INSUFFICIENT — недостаточно scope/roles
  • 404 Not Found
    • ERR_RESOURCE_NOT_FOUND — нет ресурса
    • (иногда) ERR_RESOURCE_INACCESSIBLE — ресурс скрыт из соображений безопасности (см. ниже)
  • 409 Conflict
    • ERR_CONFLICT — конфликт состояния (например, версия документа изменилась)
    • ERR_IDEMPOTENCY_CONFLICT — конфликт идемпотентности/повтор с другим телом
  • 422 Unprocessable Entity
    • ERR_VALIDATION_FAILED — нарушены бизнес-валидации
    • ERR_CONSTRAINT_VIOLATION — нарушено правило домена (не синтаксис)

Подводный камень: многие API используют 400 “за всё”. Но если вы хотите, чтобы клиенты делали корректные действия (например, ретраить/не ретраить), стоит различать “невалидный запрос” и “валидация домена”, хотя оба окажутся 4xx.

5xx: ошибки сервера

  • 500 Internal Server Error
    • ERR_INTERNAL — непредвиденная ошибка
    • ERR_UNEXPECTED_STATE — “невозможная” ветка
  • 502 Bad Gateway
    • ERR_UPSTREAM_BAD_RESPONSE — downstream вернул некорректный ответ
  • 503 Service Unavailable
    • ERR_SERVICE_UNAVAILABLE — перегрузка/пауза/maintenance
    • ERR_DEPENDENCY_UNAVAILABLE — недоступен критичный dependency
  • 504 Gateway Timeout
    • ERR_UPSTREAM_TIMEOUT — таймаут при обращении к downstream

Рекомендация по стабильности

  • code должен быть стабильным. Если меняется формат деталей (details), пусть code останется прежним.
  • message можно локализовать или менять формулировку, но клиент не должен на него опираться.
  • type/ссылка на документацию (если есть) должна не ломаться при реорганизациях.

Детализация для отладки vs безопасные сообщения

Это самый частый конфликт интересов: разработчикам нужно знать подробности, а безопасности — минимизировать раскрытие.

Два уровня сообщений: user-facing и debug-facing

Один из практичных шаблонов — хранить в ответе два слоя:

  • message (или userMessage) — безопасное объяснение, предназначенное пользователю/клиенту приложения;
  • debug — опционально, только для доверенных клиентов или включается флагом (например, если запрос содержит X-Debug-Id или вы сделали whitelisting).

Пример:

code
{
  "error": {
    "code": "ERR_VALIDATION_FAILED",
    "message": "Некорректные данные запроса.",
    "details": {
      "fieldErrors": [
        { "field": "email", "message": "Неверный формат" }
      ]
    },
    "correlationId": "b7f2b7a0-8f7f-4a1b-9c5f-3e2f0b4a6b31"
  }
}

И отдельно (для доверенного режима) — расширенная ветка:

code
{
  "error": {
    "code": "ERR_UPSTREAM_TIMEOUT",
    "message": "Сервис временно недоступен.",
    "correlationId": "5a1c3a1b-9e1e-4b0a-9e6b-1f0a2d3c4b5a",
    "debug": {
      "upstream": "payments-service",
      "timeoutMs": 3000
    }
  }
}

Что нельзя отправлять в details

Практический минимум, который лучше запретить политикой:

  • SQL-запросы, имена таблиц/колонок, фрагменты схемы;
  • stack trace, классы исключений, имена модулей;
  • внутренние URL, адреса хостов, токены;
  • “почему именно так” в логике авторизации, если это позволяет атакующему угадывать существование ресурсов.

Баланс: поле за полем

Иногда “подробности” — это просто уточнение формата. Это обычно безопасно:

  • какие поля не прошли валидацию,
  • какие диапазоны ожидаются,
  • какие ограничения по формату.

Опасно — “всё про то, как устроено внутри”.


Примеры контрактов для распространённых сценариев

Ниже несколько кейсов с разными 4xx/5xx и соответствующими кодами.

1) Невалидный JSON: 400 + ERR_INVALID_JSON

code
POST /v1/users
Content-Type: application/json

{ "email": "a@", }

Ответ:

code
{
  "error": {
    "code": "ERR_INVALID_JSON",
    "message": "Некорректный формат JSON.",
    "correlationId": "a2e9a6aa-4f9c-4aa2-8b6e-9e6d0d1c2b3a"
  }
}

Тут details обычно не нужен: парсер уже упал, и лучше не пытаться “угадать” позицию, которая может различаться между парсерами.

2) Валидация домена: 422 + ERR_VALIDATION_FAILED

Запрос валиден синтаксически, но бизнес-правила не прошли:

code
POST /v1/accounts/activate
{
  "userId": "123",
  "activationCode": "12",
  "consent": false
}

Ответ:

code
{
  "error": {
    "code": "ERR_VALIDATION_FAILED",
    "message": "Проверьте данные активации.",
    "details": {
      "fieldErrors": [
        { "field": "activationCode", "message": "Должно быть 6 символов" },
        { "field": "consent", "message": "Требуется согласие" }
      ]
    },
    "correlationId": "c0f1a2b3-c4d5-4e6f-9a0b-1c2d3e4f5a6b"
  }
}

Обратите внимание: клиенту достаточно code + fieldErrors, чтобы подсветить форму и не перебирать варианты.

3) Нет прав: 403 + ERR_PERMISSION_DENIED

code
{
  "error": {
    "code": "ERR_PERMISSION_DENIED",
    "message": "Недостаточно прав для выполнения действия.",
    "correlationId": "d1e2f3a4-b5c6-4d7e-8f90-a1b2c3d4e5f6"
  }
}

Не пытайтесь сообщать “ресурс существует, но вам нельзя”. Если существует риск утечки — формулируйте нейтрально.

4) Конфликт состояния: 409 + ERR_CONFLICT

Например, оптимистическая блокировка по версии:

code
PUT /v1/documents/55
If-Match: "3"
{
  "title": "Новый заголовок"
}

Если версия изменилась:

code
{
  "error": {
    "code": "ERR_CONFLICT",
    "message": "Конфликт версий. Обновите данные и повторите попытку.",
    "details": {
      "currentVersion": 4
    },
    "correlationId": "f1e2d3c4-b5a6-4f78-9a01-b2c3d4e5f6a7"
  }
}

Клиент может принять решение: либо перечитать ресурс, либо показать сообщение пользователю.

5) Ошибка downstream: 503 + ERR_DEPENDENCY_UNAVAILABLE

Сервис зависит от внешнего платежного шлюза.

Если downstream лежит:

code
{
  "error": {
    "code": "ERR_DEPENDENCY_UNAVAILABLE",
    "message": "Платёжный сервис временно недоступен.",
    "correlationId": "3b5a7c9d-1e2f-4a3b-8c6d-9e0f1a2b3c4d"
  }
}

Если вы хотите поддержать ретраи, добавьте HTTP заголовок Retry-After (это уже отдельный контракт на уровне протокола):

code
HTTP/1.1 503 Service Unavailable
Retry-After: 10

А code позволит клиентам отличить “временно недоступно” от “невозможно выполнить”.


Как действовать с 4xx и 5xx: ретраи, идемпотентность и “правильные” статусы

В контракте ошибок важны не только поля, но и ожидаемое поведение клиента.

Что обычно ретраить

  • 408 Request Timeout (иногда)
  • 429 Too Many Requests — часто с Retry-After
  • 502/503/504 — ретраи с backoff

В этих случаях message может быть нейтральным, но code лучше сделать предсказуемым.

Пример для rate limit:

code
{
  "error": {
    "code": "ERR_RATE_LIMITED",
    "message": "Слишком много запросов. Попробуйте позже.",
    "correlationId": "..."
  }
}

И соответствующие заголовки:

code
HTTP/1.1 429 Too Many Requests
Retry-After: 30

Что обычно НЕ ретраить

  • 400/422 — “клиент должен исправить”
  • 401/403 — “нужна авторизация/права”
  • 404 — “нет ресурса” или “недоступен” (зависит от политики)

Если клиент всё равно ретраит из-за неверной интерпретации, вы получите лавину нагрузки — поэтому договорённость важна.

Идемпотентность и 409

Для POST/PUT с идемпотентностью полезно иметь отдельный код. Например:

  • ERR_IDEMPOTENCY_CONFLICT — повтор с тем же ключом, но разными телами
  • ERR_IDEMPOTENCY_EXPIRED — ключ истёк

Так вы даёте клиенту шанс корректно разобраться в ситуации, вместо бесконечных повторов.


Как договориться со сторонними клиентами и не сломать интеграции

Интеграции чаще всего ломаются не из-за “неверного статуса”, а из-за изменения формы ответа и логики трактовки ошибок. Чтобы этого избежать, нужны принципы.

1) Версионируйте контракт ошибок

Есть два распространённых подхода:

  • Версионировать API целиком (например, /v1, /v2) и держать контракт ошибок совместимым внутри версии.
  • Версионировать формат ошибок внутри поля, например error.version: 1.

В практическом смысле проще всего — держать контракт совместимым внутри API версии и обновлять его только при major-изменениях.

2) Правило совместимости: добавляй, но не переименовывай

  • Добавляйте новые коды ошибок — это обычно безопасно.
  • Не удаляйте и не переименовывайте старые коды.
  • Если вы меняете семантику поля (например, теперь details.fieldErrors имеет другой формат), лучше ввести новое поле или новый code.

3) Document your codes

“Документация” — это не отдельная страница на 200 строк. Достаточно:

  • перечня code (минимум: смысл, какие статусы возможны, какие поля details приходят);
  • примера ответа;
  • рекомендаций для клиентов (“retry / do not retry”).

Для интеграций это критично: иначе

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

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

Перейти к курсу

Другие статьи

Тестирование API контрактом: как проверять не только статус, но и семантику ответов
тестирование

Тестирование API контрактом: как проверять не только статус, но и семантику ответов

Покажем подход к контрактным тестам: схемы ответа, обязательные поля, допустимые состояния ошибок, проверка idempotency и регрессия поведения при изменениях версий API.

18 сентября 2026 г.
1020
Как читать и писать REST API контракты: статусы, ошибки и схемы ответов
читать

Как читать и писать REST API контракты: статусы, ошибки и схемы ответов

Разберём типовые ошибки в дизайне API и научимся фиксировать контракт: коды HTTP, формат ошибок, версионирование и примеры запросов/ответов.

20 июля 2026 г.
910
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Что такое “HTTP контракт для ошибок”Почему одного статуса малоБазовый формат ошибки: поля, которые реально работаютМинимальный набор vs расширенныйКак связать HTTP-стататусы и коды ошибокТаблица: типовые сочетания 4xx/5xx и кодыРекомендация по стабильностиДетализация для отладки vs безопасные сообщенияДва уровня сообщений: user-facing и debug-facingЧто нельзя отправлять в detailsБаланс: поле за полемПримеры контрактов для распространённых сценариев1) Невалидный JSON: 400 + ERRINVALIDJSON2) Валидация домена: 422 + ERRVALIDATIONFAILED3) Нет прав: 403 + ERRPERMISSIONDENIED4) Конфликт состояния: 409 + ERRCONFLICT5) Ошибка downstream: 503 + ERRDEPENDENCYUNAVAILABLEКак действовать с 4xx и 5xx: ретраи, идемпотентность и “правильные” статусыЧто обычно ретраитьЧто обычно НЕ ретраитьИдемпотентность и 409Как договориться со сторонними клиентами и не сломать интеграции1) Версионируйте контракт ошибок2) Правило совместимости: добавляй, но не переименовывай3) Document your codes