$ 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
·28 июля 2026 г.·9 мин·68
Ошибки HTTP в проде: как строить понятный клиентский контракт (код, причина, поле и ретраи)

Покажем, как проектировать ошибки так, чтобы фронтенд и интеграции могли принимать решения: что ретраить, какие поля считать виноватыми, как различать 4xx/5xx и как стабилизировать формат ответа.

Содержание
1) HTTP-статус: это не “код ошибки”, а сигнал классификации1.1. Ключевой принцип: классифицируйте по вине и воспроизводимости1.2. Практическая схема: что отдавать в проде2) Ошибка как контракт: код, причина, поле, детали2.1. Минимальный “разумный” формат2.2. Уровни детализации: когда можно обойтись без details2.3. Ошибки домена vs инфраструктуры: разные type и code3) Правила ретраев: не только “если 5xx — повтори”3.1. Ретраи зависят от операции и идемпотентности3.2. Мини-матрица для клиента3.3. Явный retryable и “почему”3.4. Почему важны лимиты и jitter4) Отличать 4xx/5xx: что именно означает “вина клиента”4.1. 422: место, где фронтенд получает управляемость4.2. 400 vs 422: где граница4.3. 409: конфликт — отдельная ветка решений5) Стабилизация формата ответа: как не ломать клиентов5.1. Контракт должен быть версионируемым и расширяемым5.2. Пример “валидируемого” контракта (TypeScript)5.3. Нельзя “иногда возвращать массив”, “иногда объект”6) Примеры: как серверу отдавать ошибки, клиенту — решать6.1. Сервер: FastAPI (Python) пример стандартизированного ответа6.2. Клиент: логика ретраев в TypeScript (fetch + backoff)7) Типичные ошибки команд в проде (и как их лечить)7.1. Смешение 400/422: фронтенд теряет семантику

В продакшене HTTP-ошибки — это не «красные статусы в логах», а контракт между сервисами и фронтендом. Этот контракт определяет поведение клиента: нужно ли повторять запрос, какой компонент исправлять, как показать ошибку пользователю, и как логически маршрутизировать обработку внутри интеграций.

Проблема в том, что многие команды проектируют ошибки как побочный эффект реализации: где-то бросили 500, где-то забыли консистентность полей, где-то смешали бизнес-валидацию и инфраструктурные сбои, а где-то отдали «кривой» формат ответа. В результате клиенты вынуждены гадать — и начинают ретраить «всё подряд» или, наоборот, игнорировать случаи, когда повтор нужен.

Ниже — практический гайд, как строить понятный клиентский контракт для ошибок:

  • какие статусы реально использовать (4xx vs 5xx),
  • как передавать причину и поле (parameter-level blame),
  • как сделать формат стабилизированным и расширяемым,
  • как определить правила ретраев и не устроить ретрайн-шторм.

1) HTTP-статус: это не “код ошибки”, а сигнал классификации

1.1. Ключевой принцип: классифицируйте по вине и воспроизводимости

HTTP-статус должен отвечать на два вопроса:

  1. Кто виноват?
  • клиент (запрос некорректен, отсутствуют обязательные данные, нарушены правила домена)
  • сервер (не смог выполнить, сломалась зависимость, истёк таймаут, внутренняя ошибка)
  1. Можно ли исправить повтором?
  • да (например, временная недоступность)
  • нет (например, неверный формат JSON или невалидный идентификатор по формату/правилам)

Это напрямую влияет на ретраи.

1.2. Практическая схема: что отдавать в проде

Типовая модель:

  • 400 Bad Request — запрос синтаксически неверен или не проходит базовую схему (например, JSON не распарсился).
  • 401 Unauthorized — нет аутентификации (нет/плохой токен). Клиент может обновить токен.
  • 403 Forbidden — аутентификация есть, но нет прав/доступа. Повтор обычно бесполезен.
  • 404 Not Found — ресурс отсутствует (или вы намеренно скрываете наличие).
  • 409 Conflict — конфликт состояния (версии, идемпотентность, бизнес-конфликт).
  • 422 Unprocessable Entity — семантически невалидные данные (валидация домена). Это особенно полезно, когда вы хотите вернуть список проблем по полям.
  • 429 Too Many Requests — rate limit. Здесь ретраи возможны, но только с учетом Retry-After.
  • 500 Internal Server Error — внутренняя ошибка, которую клиент не обязан исправлять.
  • 502 Bad Gateway — ошибка прокси/шлюза при обращении к upstream (зависимость от другого сервиса).
  • 503 Service Unavailable — сервис временно недоступен/перегружен (часто лучше для “ретраить после паузы”).
  • 504 Gateway Timeout — таймаут при обращении к upstream.

Почему это важно

Клиентская логика в интеграциях часто строится на статус-кодах:

  • для 5xx — retry (с бэкоффом и ограничением),
  • для части 4xx — немедленный fail,
  • для 401 — refresh токена,
  • для 422 — показать пользователю ошибки по полям.

Если размыть границы, клиент будет либо делать лишние повторные запросы (усугубляя падение), либо не повторит там, где нужно.


2) Ошибка как контракт: код, причина, поле, детали

HTTP-статус — верхнеуровневая классификация. Но клиенту нужно больше структуры. В проде обычно нужен JSON-объект, который:

  • легко парсится,
  • имеет стабильные поля,
  • позволяет клиенту принять решение (retry? validate fields? show message?).

2.1. Минимальный “разумный” формат

Одна из рабочих схем для REST API:

code
{
  "error": {
    "type": "validation_error",
    "code": "USER_EMAIL_INVALID",
    "message": "Email не проходит валидацию",
    "requestId": "req_01J3...",
    "details": [
      {
        "field": "email",
        "reason": "invalid_format"
      }
    ],
    "retryable": false
  }
}

Где:

  • type — машинно-читаемая категория (валидация, конфликт, недоступность и т.д.)
  • code — стабильный код ошибки домена (не меняйте без осознанной версии)
  • message — человекочитаемое (желательно для логов/в UI; но UI всё равно часто локализует по code)
  • details — список проблем (особенно для 422)
  • retryable — явный флаг (не обязательно, но полезно для клиентов)
  • requestId — корреляция для дебага

Важно: “field” — это не просто имя параметра

Речь о blame: клиент должен понимать, что именно исправлять в запросе. Поэтому field лучше поддерживать:

  • согласованным с API-контрактом (например, email, password, profile.name),
  • соответствующим способу передачи (JSON body / query / path),
  • безопасным (не раскрывать внутренние поля БД).

2.2. Уровни детализации: когда можно обойтись без details

  • Для общих ошибок типа “не удалось выполнить” без привязки к полям можно ограничиться code и message.
  • Для 422 почти всегда нужны details, иначе фронтенд не сможет связать ошибку с конкретными input-ами.

2.3. Ошибки домена vs инфраструктуры: разные type и code

Пример:

  • Доменные:
    • 422 + type: validation_error + code: ORDER_ITEMS_OUT_OF_STOCK
  • Инфраструктурные:
    • 503 + type: dependency_unavailable + code: INVENTORY_TIMEOUT

Это позволяет клиенту принимать решения даже при одинаковых статусах.


3) Правила ретраев: не только “если 5xx — повтори”

3.1. Ретраи зависят от операции и идемпотентности

Даже если статус 503 говорит “можно повторить”, клиент должен учитывать:

  • метод: GET обычно безопаснее ретраить, POST — осторожно
  • идемпотентность: если сервер поддерживает Idempotency-Key, ретраи безопаснее
  • side effects: платёж, создание заявки, отправка письма

Если у вас нет идемпотентности, ретраи POST могут создавать дубликаты.

3.2. Мини-матрица для клиента

Рекомендуемая логика со стороны клиента/интеграции:

  • 429: ретрай после Retry-After (если он есть), иначе fail или с дефолтной паузой.
  • 5xx: ретрай с экспоненциальным backoff + jitter, максимум N попыток.
  • 502/503/504: ретрай чаще, потому что обычно это “upstream временно”.
  • 4xx: обычно не ретраить (кроме случаев, где это явно разрешено, например 409 может быть ретраен после получения актуального состояния, но это уже бизнес-логика).

3.3. Явный retryable и “почему”

Один из способов стабилизировать поведение — включать retryable и retryAfterMs (или Retry-After в заголовке).

code
{
  "error": {
    "type": "dependency_timeout",
    "code": "PAYMENT_PROVIDER_TIMEOUT",
    "message": "Платёжный провайдер не ответил вовремя",
    "retryable": true,
    "retryAfterMs": 800
  }
}

Если retryable: true, клиенту легче договориться с контрактом без “угадываний” по статусу.

3.4. Почему важны лимиты и jitter

Без jitter и лимитов в микросервисах легко получить ретрай-шторм: один сбой → миллионы повторов → каскадное ухудшение.

Типовая стратегия:

  • backoff: base * 2^attempt
  • jitter: перемешать в диапазоне (например, 0.8–1.2)
  • max attempts: 3–5 (зависит от SLA)
  • общий cap по времени: например, 10–20 секунд суммарно

4) Отличать 4xx/5xx: что именно означает “вина клиента”

4.1. 422: место, где фронтенд получает управляемость

Статус 422 Unprocessable Entity полезен как “контракт валидации”: запрос распарсился, но бизнес-семантика не принята.

Пример ответа для “неверные поля”:

code
{
  "error": {
    "type": "validation_error",
    "code": "REGISTRATION_DATA_INVALID",
    "message": "Не удалось создать пользователя",
    "retryable": false,
    "details": [
      { "field": "email", "reason": "invalid_format" },
      { "field": "password", "reason": "too_short", "minLength": 8 },
      { "field": "termsAccepted", "reason": "required" }
    ]
  }
}

Клиент:

  • подсвечивает поля email, password, termsAccepted
  • не ретраит
  • может локализовать сообщения по reason и code

4.2. 400 vs 422: где граница

  • 400 — “я не понял запрос” (syntax, типы на уровне формата, например Content-Type не совпадает, JSON не парсится, обязательное поле отсутствует при схеме).
  • 422 — “запрос понял, но не согласен с содержимым домена”.

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

4.3. 409: конфликт — отдельная ветка решений

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

Пример:

  • обновление ресурса по версии
  • создание сущности, где возможен дедуп по внешнему ключу
code
{
  "error": {
    "type": "conflict",
    "code": "RESOURCE_VERSION_MISMATCH",
    "message": "Данные изменились, обновите перед повтором",
    "retryable": false,
    "details": [
      { "field": "version", "reason": "stale" }
    ]
  }
}

Клиент может:

  • перезагрузить ресурс,
  • повторить запрос с новой версией,
  • показать “обновите страницу” (или тихо пересинхронизировать).

5) Стабилизация формата ответа: как не ломать клиентов

5.1. Контракт должен быть версионируемым и расширяемым

Не стоит менять структуру “внезапно”. Хорошая практика:

  • фиксировать базовые поля (error.type, error.code, error.retryable)
  • добавлять поля только назад-совместимо
  • не переименовывать field → path и т.п. без версионности

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

5.2. Пример “валидируемого” контракта (TypeScript)

code
export type ApiErrorDetail = {
  field?: string;
  reason: string;
  [key: string]: unknown;
};

export type ApiErrorResponse = {
  error: {
    type: string;
    code: string;
    message: string;
    retryable?: boolean;
    retryAfterMs?: number;
    requestId?: string;
    details?: ApiErrorDetail[];
  };
};

Клиент:

  • может безопасно игнорировать поля, которые не знает
  • строит UI по details.field и reason

5.3. Нельзя “иногда возвращать массив”, “иногда объект”

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

  • в одном сервисе details — массив,
  • в другом — объект,
  • в третьем — строка.

Стабилизация — это не “красивый JSON”, а сохранение целостности интеграций.


6) Примеры: как серверу отдавать ошибки, клиенту — решать

6.1. Сервер: FastAPI (Python) пример стандартизированного ответа

code
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel
import uuid

app = FastAPI()

class ErrorDetail(BaseModel):
    field: str | None = None
    reason: str
    minLength: int | None = None

class ErrorBody(BaseModel):
    type: str
    code: str
    message: str
    retryable: bool | None = None
    retryAfterMs: int | None = None
    requestId: str | None = None
    details: list[ErrorDetail] | None = None

def build_error_response(status_code: int, err: ErrorBody):
    request_id = err.requestId or f"req_{uuid.uuid4().hex[:10]}"
    payload = {"error": {**err.model_dump(exclude_none=True), "requestId": request_id}}
    return JSONResponse(status_code=status_code, content=payload)

@app.post("/register")
async def register(payload: dict, request: Request):
    email = payload.get("email")
    password = payload.get("password")

    details = []
    if not email or "@" not in email:
        details.append({"field": "email", "reason": "invalid_format"})
    if not password or len(password) < 8:
        details.append({"field": "password", "reason": "too_short", "minLength": 8})

    if details:
        err = ErrorBody(
            type="validation_error",
            code="REGISTRATION_DATA_INVALID",
            message="Не удалось создать пользователя",
            retryable=False,
            details=details
        )
        return build_error_response(422, err)

    return {"ok": True}

@app.get("/payments/status/{id}")
async def payment_status(id: str):
    # допустим, upstream недоступен
    err = ErrorBody(
        type="dependency_unavailable",
        code="PAYMENT_UPSTREAM_TIMEOUT",
        message="Провайдер не ответил вовремя",
        retryable=True,
        retryAfterMs=800
    )
    return build_error_response(503, err)

Что это даёт:

  • стабильная структура {"error": {...}}
  • retryable явно отвечает на вопрос “повторять или нет”
  • details позволяет фронтенду привязать ошибки к полям

6.2. Клиент: логика ретраев в TypeScript (fetch + backoff)

code
type ApiErrorResponse = {
  error: {
    code: string;
    message: string;
    retryable?: boolean;
    retryAfterMs?: number;
    details?: Array<{ field?: string; reason: string }>;
  };
};

function sleep(ms: number) {
  return new Promise((r) => setTimeout(r, ms));
}

function withJitter(ms: number) {
  const jitter = 0.8 + Math.random() * 0.4; // 0.8..1.2
  return Math.round(ms * jitter);
}

export async function requestWithRetry(
  input: RequestInfo,
  init: RequestInit & { maxAttempts?: number } = {}
) {
  const maxAttempts = init.maxAttempts ?? 4;
  let attempt = 0;
  let lastError: unknown;

  while (attempt < maxAttempts) {
    attempt += 1;

    try {
      const res = await fetch(input, init);
      if (!res.ok) {
        let body: ApiErrorResponse | null = null;
        try {
          body = (await res.json()) as ApiErrorResponse;
        } catch {
          // не-JSON ответ — всё равно решаем по статусу
        }

        const retryable =
          body?.error.retryable ??
          (res.status >= 500 && res.status <= 599) ||
          res.status === 429;

        const retryAfterMs =
          body?.error.retryAfterMs ??
          (res.headers.get("Retry-After")
            ? Number(res.headers.get("Retry-After")) * 1000
            : undefined);

        if (retryable && attempt < maxAttempts) {
          const base = retryAfterMs ?? 200 * 2 ** (attempt - 1);
          await sleep(withJitter(base));
          continue;
        }

        throw { status: res.status, body };
      }

      return await res.json();
    } catch (e) {
      lastError = e;
      if (attempt >= maxAttempts) break;
    }
  }

  throw lastError;
}

Важно: клиент учитывает retryable из тела ошибки, а не только статус. Это снижает количество неверных ретраев, если сервер отдал 4xx/5xx нетипично.


7) Типичные ошибки команд в проде (и как их лечить)

7.1. Смешение 400/422: фронтенд теряет семантику

Симптом:

  • иногда валидация возвращает 400,
  • иногда 422,
  • иногда ошибки без details.

Лечение:

  • договориться о границе: 400 = синтаксис/формат,

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

Автор

$ 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 с токеном, голосованием и казной.

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

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

Python для аналитики на практике: как строить признаки и проверять их воспроизводимость
python

Python для аналитики на практике: как строить признаки и проверять их воспроизводимость

Поймём, где чаще всего ломается воспроизводимость (версии данных, фильтры, дрейф) и как оформить пайплайн признаков так, чтобы результаты совпадали между запусками и окружениями.

17 сентября 2026 г.
1780
Как настроить локальную среду для Python-проекта: venv, pyproject.toml и воспроизводимые зависимости
настроить

Как настроить локальную среду для Python-проекта: venv, pyproject.toml и воспроизводимые зависимости

Разберём, как правильно собрать окружение для разработки и CI: структуры проекта, pyproject.toml, lock-файлы и типовые сценарии воспроизведения зависимостей на другой машине. Параллельно покажем, как избежать “работает у меня”.

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

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

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

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

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

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

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

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

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

18 сентября 2026 г.
1940
Что такое SQL и зачем он нужен новичку: как думать о данных простыми словами
такое

Что такое SQL и зачем он нужен новичку: как думать о данных простыми словами

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

28 октября 2026 г.
220

Комментарии

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

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

Содержание

1) HTTP-статус: это не “код ошибки”, а сигнал классификации1.1. Ключевой принцип: классифицируйте по вине и воспроизводимости1.2. Практическая схема: что отдавать в проде2) Ошибка как контракт: код, причина, поле, детали2.1. Минимальный “разумный” формат2.2. Уровни детализации: когда можно обойтись без details2.3. Ошибки домена vs инфраструктуры: разные type и code3) Правила ретраев: не только “если 5xx — повтори”3.1. Ретраи зависят от операции и идемпотентности3.2. Мини-матрица для клиента3.3. Явный retryable и “почему”3.4. Почему важны лимиты и jitter4) Отличать 4xx/5xx: что именно означает “вина клиента”4.1. 422: место, где фронтенд получает управляемость4.2. 400 vs 422: где граница4.3. 409: конфликт — отдельная ветка решений5) Стабилизация формата ответа: как не ломать клиентов5.1. Контракт должен быть версионируемым и расширяемым5.2. Пример “валидируемого” контракта (TypeScript)5.3. Нельзя “иногда возвращать массив”, “иногда объект”6) Примеры: как серверу отдавать ошибки, клиенту — решать6.1. Сервер: FastAPI (Python) пример стандартизированного ответа6.2. Клиент: логика ретраев в TypeScript (fetch + backoff)7) Типичные ошибки команд в проде (и как их лечить)7.1. Смешение 400/422: фронтенд теряет семантику