$ 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
·24 июля 2026 г.·10 мин·69
HTTP-статусы и тело ответа: как сделать ошибки предсказуемыми для клиентов

Построим согласованный формат ошибок: коды, message, details и корреляционные идентификаторы. Разберём, как не отдавать разные структуры в зависимости от маршрута.

Содержание
Почему клиенты «не любят» ошибки (и где именно всё ломается)Базовый принцип: ошибки — это не исключения, а публичный контрактСтатусы: как выбрать код, который клиент сможет интерпретировать400 vs 422: валидация и семантика401 и 403: аутентификация и авторизация404 и «скрытие существования»409: конфликты422/409/400 при доменных ошибках500: неожиданные ошибкиЕдиный формат ошибки: коды, message, details и correlationIdcode: стабильный идентификатор ошибки для логики клиентаmessage: человекочитаемое объяснение без раскрытия деталейdetails: расширение для валидации и множественных проблемcorrelationId: ключ для связки ответа и логовКак не отдавать разные структуры в зависимости от маршрутаЧто обычно ломается на практикеРешение: централизованный error mapperКонтроль: договор и автоматические тестыКорреляция: как сделать correlationId полезнымПример полного контракта: единые ошибки для типовых сценариевНеверный формат тела (400/422)Нет прав (403)Не найден (404)Внутренняя ошибка (500)Подводные камни: что часто забывают1) Не отдавайте JSON там, где может быть HTML2) Не меняйте схему details без версии контракта3) Не используйте текст как код4) Следите за консистентностью статусов5) Скрывайте внутренние детали в 500

Ошибки — это тот участок API, который чаще всего «плывёт» со временем. Сегодня один маршрут отдаёт {error: "..."}, завтра другой — {message: "...", code: ...}, а ещё через месяц где-то появляется HTML-страница или пустой ответ с 500. Клиенту в таких условиях приходится угадывать структуру: где-то распознавать по заголовкам, где-то по форме JSON, где-то по тексту. В результате растут стоимость поддержки, количество обработчиков «для особых случаев» и вероятность того, что критические ошибки будут проигнорированы клиентом.

Цель этой статьи — показать, как построить согласованный формат ошибок для HTTP API: выбрать корректные статусы, определить тело ответа (например, message и details), добавить корреляционные идентификаторы для трассировки и, главное, добиться предсказуемости: один и тот же контракт ошибок должен возвращаться независимо от маршрута. Это не теория «как правильно», а практический подход, который обычно внедряют после того, как API начинает обрастать количеством endpoints и разрозненными обработчиками.


Почему клиенты «не любят» ошибки (и где именно всё ломается)

Реальная боль клиентов обычно не в самом факте ошибки — а в непредсказуемости. Непредсказуемость проявляется в трёх измерениях:

  1. HTTP-статус не соответствует телу
    Например, статус 400 вместе с телом, где «ошибка» выглядит как системная (traceId отсутствует, поля несогласованы), или наоборот. Клиентские библиотеки часто строятся вокруг статусов: 4xx обрабатывают как «вина клиента», 5xx — как «вина сервера».

  2. Слишком много форматов ошибок
    Часто встречается ситуация: валидация отправляет одно, бизнес-правила — другое, исключения фреймворка — третье, а неожиданные ошибки могут возвращать пустое тело или HTML.

  3. Отсутствие корреляции между ответом и логами
    Клиенту нужно сообщить, что именно пошло не так. Разработчикам — быстро найти инцидент в логах. Без идентификатора это превращается в обмен сообщениями вида «у нас на фронте упало, где-то, кажется, был 500».

Чтобы ошибки стали «потребляемыми», контракт должен быть стабильным: структура ответа и смысл полей неизменны, меняются только значения.


Базовый принцип: ошибки — это не исключения, а публичный контракт

Внутри сервера могут быть любые исключения, но наружу API обязано отдавать единый формат. Это означает:

  • один слой отвечает за маппинг исключений/ошибок на HTTP-статусы;
  • второй слой формирует тело по единой схеме;
  • третий слой обеспечивает, чтобы независимо от маршрута контракт не разъезжался.

На практике это часто реализуют через централизованный middleware/handler (в зависимости от стека), который перехватывает исключения, собирает контекст и возвращает JSON в заранее определённом формате.


Статусы: как выбрать код, который клиент сможет интерпретировать

HTTP-статусы — это соглашение между клиентом и сервером. Они не обязаны быть идеальными на 100%, но должны быть последовательными и объясняющими поведение.

400 vs 422: валидация и семантика

  • 400 Bad Request — запрос синтаксически неверен: JSON не парсится, отсутствуют обязательные поля, неверный формат заголовка и т. п.
  • 422 Unprocessable Entity — запрос синтаксически корректен, но семантически не проходит валидацию бизнес-правил/формата данных (например, поле email не соответствует формату, длина строки вне диапазона, enum не совпадает).
    В некоторых системах 422 не используют — тогда везде применяют 400. Но важно: выбрав стратегию, придерживаться её.

Валидация обычно возвращает структурированные детали: список полей и сообщений.

401 и 403: аутентификация и авторизация

  • 401 Unauthorized — клиент не аутентифицировался (нет токена, токен неверный/просроченный). Часто добавляют WWW-Authenticate.
  • 403 Forbidden — клиент аутентифицирован, но не имеет прав. Тело ответа может помочь понять, какие действия недоступны (но не раскрывать лишнее).

404 и «скрытие существования»

  • 404 Not Found — ресурс не найден.
    Иногда системы сознательно возвращают 404 вместо 403, чтобы «не светить» существование ресурса. Это влияет на контракт ошибок: клиенту важно понимать, что именно означает 404 в вашем домене.

409: конфликты

  • 409 Conflict — конфликт состояния: например, попытка изменить ресурс, который изменился параллельно; попытка создать дубликат, нарушающий уникальность.

422/409/400 при доменных ошибках

Доменные ошибки часто сложно разложить строго по стандарту, но правило простое:

  • если ошибка связана с форматом/валидацией ввода — ближе к 400/422;
  • если ошибка — про текущее состояние/ограничения — ближе к 409 или 422 (зависит от смысла).

500: неожиданные ошибки

  • 500 Internal Server Error — всё, что не удалось классифицировать.
    Важно: в ответе не отдавать stack trace, SQL, подробные внутренние сообщения. Клиенту достаточно message + correlationId, а подробности — в логах.

Единый формат ошибки: коды, message, details и correlationId

Один из самых практичных вариантов — сделать тело ответа предсказуемым и машиночитаемым.

Ниже — пример контракта:

code
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "path": "user.email",
        "message": "Invalid email format",
        "type": "FORMAT"
      }
    ],
    "correlationId": "01J6Y7K9Q3X2A1B4C5D6E7F8"
  }
}

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

code: стабильный идентификатор ошибки для логики клиента

  • Это не HTTP-статус. Это доменный/контрактный код, который не должен меняться от версии к версии без причины.
  • Клиент использует code для отображения локализованных сообщений, подсказок или для ветвления логики.

Примеры кодов:

  • VALIDATION_ERROR
  • AUTHENTICATION_REQUIRED
  • AUTHORIZATION_DENIED
  • RESOURCE_NOT_FOUND
  • CONFLICT
  • INTERNAL_ERROR

message: человекочитаемое объяснение без раскрытия деталей

  • message — для пользователя/администратора, но в API он также полезен как быстрый сигнал клиенту.
  • Сообщение должно быть корректным, но не подробным: не показывать внутренние названия таблиц, запросы, stack trace.

details: расширение для валидации и множественных проблем

  • Поле details критично для ошибок валидации: клиенту нужно понять, какие поля и почему не прошли.
  • Структуру details можно расширять, но важно сохранять общий подход: details — массив или объект с понятной схемой, а не произвольный JSON.

Частая ошибка — отдавать details как строку или как объект разной структуры в зависимости от маршрута. Это возвращает нас к исходной проблеме.

correlationId: ключ для связки ответа и логов

  • Идентификатор, который сервер генерирует для конкретного инцидента.
  • Его клиент может включить в тикет, а поддержка/разработчики — использовать для поиска в логах.
  • Хорошая практика — добавлять корреляционный ID не только в тело, но и в заголовок (например, X-Correlation-Id), чтобы клиент мог логировать его даже при невалидном JSON.

Как не отдавать разные структуры в зависимости от маршрута

Ключевая мысль: формат ошибки должен быть единым для всего API, а различаться могут только значения и количество элементов в details.

Что обычно ломается на практике

  1. Валидация сделана фреймворком, бизнес-ошибки — вручную
    Валидация отдаёт {message: ..., errors: ...}, а бизнес — {error: ...}.

  2. Catch блок на одном маршруте
    Где-то есть try/catch, а где-то — нет. В результате для одинаковых категорий ошибок появляются разные ответы.

  3. Разные middleware для разных префиксов
    Например, /v1 обрабатывается одним слоем, /admin — другим, и они возвращают разный формат.

  4. Непредусмотренный fallback
    Когда исключение не попало ни в одну категорию, сервер возвращает стандартную страницу или пустой ответ — и клиент ломается.

Решение: централизованный error mapper

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

Ниже пример (условный) на Node.js/TypeScript в стиле Express-обработчика. Он показывает принципы; конкретная реализация зависит от фреймворка.

code
type ErrorDetail = {
  path?: string;
  message: string;
  type?: string;
};

type ApiErrorBody = {
  error: {
    code: string;
    message: string;
    details?: ErrorDetail[];
    correlationId: string;
  };
};

function correlationIdFromReq(req: any): string {
  // пример: если клиент прислал X-Correlation-Id — используем, иначе генерируем
  const incoming = req.headers["x-correlation-id"];
  return typeof incoming === "string" && incoming.length > 0
    ? incoming
    : crypto.randomUUID();
}

function toApiErrorBody(params: {
  code: string;
  message: string;
  details?: ErrorDetail[];
  correlationId: string;
}): ApiErrorBody {
  return {
    error: {
      code: params.code,
      message: params.message,
      ...(params.details ? { details: params.details } : {}),
      correlationId: params.correlationId,
    },
  };
}

// Пример классификации
function classifyError(err: any) {
  // Здесь вы маппите ваши доменные типы/исключения на контрактные коды и статусы
  if (err?.name === "ValidationError") {
    return {
      httpStatus: 422,
      code: "VALIDATION_ERROR",
      message: "Request validation failed",
      details: err.details as ErrorDetail[],
    };
  }

  if (err?.name === "UnauthorizedError") {
    return {
      httpStatus: 401,
      code: "AUTHENTICATION_REQUIRED",
      message: "Authentication is required",
    };
  }

  if (err?.name === "ForbiddenError") {
    return {
      httpStatus: 403,
      code: "AUTHORIZATION_DENIED",
      message: "Access is denied",
    };
  }

  if (err?.name === "NotFoundError") {
    return {
      httpStatus: 404,
      code: "RESOURCE_NOT_FOUND",
      message: "Resource not found",
    };
  }

  if (err?.name === "ConflictError") {
    return {
      httpStatus: 409,
      code: "CONFLICT",
      message: "Request conflicts with current state",
    };
  }

  return {
    httpStatus: 500,
    code: "INTERNAL_ERROR",
    message: "Internal server error",
  };
}

// Централизованный обработчик ошибок
function errorMiddleware(err: any, req: any, res: any, _next: any) {
  const correlationId = correlationIdFromReq(req);
  const classified = classifyError(err);

  // Логи — с correlationId, но без раскрытия деталей пользователю
  // logger.error({ err, correlationId, route: req.originalUrl });

  const body = toApiErrorBody({
    code: classified.code,
    message: classified.message,
    details: classified.details,
    correlationId,
  });

  res.setHeader("X-Correlation-Id", correlationId);
  res.status(classified.httpStatus).json(body);
}

Что важно здесь:

  • errorMiddleware формирует всегда один и тот же формат.
  • Даже для 500 используется тот же контейнер {"error": {...}}.
  • correlationId присутствует всегда.
  • Детали (details) добавляются только там, где они есть, но структура не меняется.

Контроль: договор и автоматические тесты

Чтобы не «сломать» формат случайно, стоит добавить контрактные тесты:

  • один тест на то, что для набора ошибок ответ всегда содержит error.code, error.message, error.correlationId;
  • тесты на то, что для валидации details — массив, а каждый элемент имеет path/message/type (или вашу схему).

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


Корреляция: как сделать correlationId полезным

Корреляционный идентификатор — это не просто красивый UUID в ответе. Он должен участвовать в цепочке:

  1. Приоритет входящего заголовка
    Если клиент отправляет X-Correlation-Id, сервер может использовать его, чтобы весь процесс был связан единым ID. Если не отправляет — сервер генерирует.

  2. Постоянство на уровне запроса
    Все логи внутри обработки одного запроса должны включать этот ID.

  3. Интеграция с мониторингом
    В идеале корреляция должна попадать в tracing (OpenTelemetry) и Sentry/Datadog. Тогда вы соединяете API-ошибку, тайминги и трассы.

  4. Не путать с requestId
    Иногда системы уже имеют requestId для внутреннего use-case. Можно иметь два идентификатора, но для клиента обычно лучше один «публичный» и стабильный: correlationId.


Пример полного контракта: единые ошибки для типовых сценариев

Рассмотрим несколько ответов, показывающих согласованность.

Неверный формат тела (400/422)

code
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
X-Correlation-Id: 01J6Y7K9Q3X2A1B4C5D6E7F8
code
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "path": "user.email", "message": "Invalid email format", "type": "FORMAT" },
      { "path": "user.age", "message": "Must be greater than or equal to 18", "type": "MIN" }
    ],
    "correlationId": "01J6Y7K9Q3X2A1B4C5D6E7F8"
  }
}

Нет прав (403)

code
HTTP/1.1 403 Forbidden
Content-Type: application/json
X-Correlation-Id: 01J6Y7K9Q3X2A1B4C5D6E7F8
code
{
  "error": {
    "code": "AUTHORIZATION_DENIED",
    "message": "Access is denied",
    "correlationId": "01J6Y7K9Q3X2A1B4C5D6E7F8"
  }
}

Не найден (404)

code
{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "Resource not found",
    "correlationId": "01J6Y7K9Q3X2A1B4C5D6E7F8"
  }
}

Внутренняя ошибка (500)

code
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
X-Correlation-Id: 01J6Y7K9Q3X2A1B4C5D6E7F8
code
{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "Internal server error",
    "correlationId": "01J6Y7K9Q3X2A1B4C5D6E7F8"
  }
}

Обратите внимание: клиент всегда получает один и тот же каркас error. Это снижает стоимость интеграции и уменьшает количество ветвлений в клиентском коде.


Подводные камни: что часто забывают

1) Не отдавайте JSON там, где может быть HTML

Если у вас есть плагины/шлюзы, которые иногда отдают HTML при ошибках (например, прокси, gateway, reverse proxy), убедитесь, что на уровне вашего API ошибки форматируются одинаково. Иначе клиент для некоторых 5xx может получать не JSON.

2) Не меняйте схему details без версии контракта

Если details в одном месте массив объектов с path, а в другом — объект с разными ключами, клиенты начнут писать парсеры под маршрут. Даже если сейчас у вас всего несколько endpoints — мигрируйте к единой форме.

3) Не используйте текст как код

Текст message со временем меняется: перевод, улучшения формулировок, форматирование. Ветка логики клиента должна опираться на code, а не на message.

4) Следите за консистентностью статусов

Если валидационная ошибка иногда отдаёт 400, иногда 422, клиентские обработчики будут усложняться. Стандартизируйте стратегию и закрепите её тестами.

5) Скрывайте внутренние детали в 500

Не отправляйте пользователю:

  • stack trace,
  • SQL,
  • сырые сообщения из исключений,
  • персональные данные из

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

Автор

$ sudo teach IT

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

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

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

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

Перейти к курсу
Solidity DLC - поймай, если сможешь

Solidity DLC - поймай, если сможешь

Аудит смарт-контрактов на реальных взломах. The DAO, Parity, Wormhole, Ronin, Euler и ещё 7 разборов: что пошло не так, сколько потеряли и что бы спасло. Атаки и защиты на Foundry, план входа в профессию аудитора.

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

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

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

Перейти к курсу
Solidity pro - всё или ничего

Solidity pro - всё или ничего

Как пишут контракты, которые работают в основной сети. Наследование и OpenZeppelin, паттерны продакшена, оптимизация газа, ABI, delegatecall и подписи. 7 модулей, 33 урока, а в финале собственное dApp: мини-DAO с токеном, голосованием и казной.

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

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

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

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

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

20 июля 2026 г.
1380
Справочник по HTTP-заголовкам: как правильно выбирать Content-Type, Cache-Control и Authorization
справочник

Справочник по HTTP-заголовкам: как правильно выбирать Content-Type, Cache-Control и Authorization

Сконцентрируемся на заголовках, которые чаще всего ломают интеграции и кэширование: что они означают и какие комбинации реально безопасны.

23 июля 2026 г.
820
Как выучить программирование самому и бесплатно: с чего начать и в каком порядке
выучить

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

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

21 сентября 2026 г.
1950
Как начать с HTML и сделать первую страницу: пошагово и без сложных слов
html

Как начать с HTML и сделать первую страницу: пошагово и без сложных слов

Разберём, что нужно новичку, чтобы за 1–2 вечера собрать свою первую веб-страницу: структура HTML, базовые теги, ссылки и картинки, плюс простой чек-лист, как понять, что страница работает правильно.

2 ноября 2026 г.
10
Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей
cargo

Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей

Разберём, как организовать Rust workspace, управлять optional features, фиксировать версии библиотек и уменьшать “дрейф” зависимостей при командной разработке и CI.

18 сентября 2026 г.
1950
Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах
такое

Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах

Разберём, из чего состоит веб-страница: что делает HTML (смысл и структура) и что делает CSS (оформление). В конце соберёте мини-страницу и поймёте, что нужно учить в каком порядке.

30 октября 2026 г.
110

Комментарии

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

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

Содержание

Почему клиенты «не любят» ошибки (и где именно всё ломается)Базовый принцип: ошибки — это не исключения, а публичный контрактСтатусы: как выбрать код, который клиент сможет интерпретировать400 vs 422: валидация и семантика401 и 403: аутентификация и авторизация404 и «скрытие существования»409: конфликты422/409/400 при доменных ошибках500: неожиданные ошибкиЕдиный формат ошибки: коды, message, details и correlationIdcode: стабильный идентификатор ошибки для логики клиентаmessage: человекочитаемое объяснение без раскрытия деталейdetails: расширение для валидации и множественных проблемcorrelationId: ключ для связки ответа и логовКак не отдавать разные структуры в зависимости от маршрутаЧто обычно ломается на практикеРешение: централизованный error mapperКонтроль: договор и автоматические тестыКорреляция: как сделать correlationId полезнымПример полного контракта: единые ошибки для типовых сценариевНеверный формат тела (400/422)Нет прав (403)Не найден (404)Внутренняя ошибка (500)Подводные камни: что часто забывают1) Не отдавайте JSON там, где может быть HTML2) Не меняйте схему details без версии контракта3) Не используйте текст как код4) Следите за консистентностью статусов5) Скрывайте внутренние детали в 500