Обработка ошибок и коды статусов
1) Зачем стандартизировать ошибки
Единый контракт ошибок ускоряет отладку клиентов, снижает ложные ретраи и делает RCA воспроизводимым. Хорошая система:- предсказуемо кодирует тип проблемы,
- дает клиенту действуемые подсказки (что делать дальше),
- защищает от утечки внутренних деталей,
- совместима с ретраями и идемпотентностью.
2) Принципы дизайна
1. Одна схема ошибок для всех сервисов (REST/GraphQL/gRPC/webhooks).
2. Четкая семантика ретраев: какие коды ретраить, какие — нет.
3. Fail-closed на write-операциях: лучше 4xx/5xx, чем тихая неконсистентность.
4. Без утечек: не раскрывать SQL, стеки, конфиги, внутренние ID.
5. Трассировка: всегда возвращайте `trace_id`/`correlation_id`.
6. Локализация сообщений — опционально, но коды и `reason` остаются стабильными.
3) Единый формат (Problem Details / JSON)
Рекомендуемый базовый формат (совместим с RFC 7807):json
{
"type": "https://errors.example.com/auth/invalid-token",
"title": "Invalid access token",
"status": 401,
"code": "AUTH_INVALID_TOKEN",
"detail": "Token expired or signature invalid.",
"instance": "/api/v1/payments/12345",
"trace_id": "01HX3...ABC",
"hint": "Obtain a new token via OAuth2 refresh.",
"meta": {
"scope": "payments:write",
"policy": "deny-by-default"
}
}
Пояснения:
- `type` — стабильный URL-идентификатор класса ошибок.
- `code` — короткий машинный код домена (стабилен между релизами).
- `hint` — что делать клиенту (повтор, обновить токен, изменить параметры).
- `meta` — безопасные детали (без секретов и PII).
4) Карта кодов статусов (минимальный набор)
Аутентификация/авторизация
400 Bad Request — структурная валидация/схема.
401 Unauthorized — нет/невалиден токен. Добавляйте `WWW-Authenticate`.
403 Forbidden — аутентифицирован, но нет прав/политики отказали.
404 Not Found — маскируйте существование ресурса при отсутствии прав.
409 Conflict — конфликт версий/состояний (optimistic lock, идемпотентность).
451 Unavailable For Legal Reasons — блок по комплаенсу/юрисдикции.
Лимиты и защита
408 Request Timeout — клиент слишком медленно отправляет тело.
409/ 425 Too Early — запрет раннего повтора в 0-RTT/TLS 1.3.
429 Too Many Requests — с `Retry-After` и политикой лимита.
499 Client Closed Request — (на периметре/NGINX) клиент разорвал соединение.
Данные и бизнес-правила
422 Unprocessable Content — бизнес-валидация прошла схему, но смысл неверен.
423 Locked — ресурс заблокирован (KYC review, AML freeze).
409 Conflict — двойная отправка, гонка, лимит состояния (например, «уже в обработке»).
410 Gone — эндпоинт/ресурс удален (депрекейт завершен).
Серверные
500 Internal Server Error — неизвестная ошибка; не раскрывать детали.
502 Bad Gateway — зависимость вернула ошибку/проксирование.
503 Service Unavailable — деградация/плановые работы; добавить `Retry-After`.
504 Gateway Timeout — таймаут зависимостей.
5) Семантика ретраев и идемпотентности
Нельзя ретраить: 400/401/403/404/422 (если клиент не изменил запрос).
Можно ретраить: 408/429/5xx/425/499/504 (с backoff + jitter).
Идемпотентность: для `POST` включайте `Idempotency-Key` (UUIDv4).
На конфликт повторного выполнения возвращайте 409 с `hint: "Use same Idempotency-Key or GET status"`.
Добавляйте `Idempotency-Replay: true` при возврате сохраненного результата.
HTTP/1.1 429 Too Many Requests
Retry-After: 3
RateLimit-Limit: 50
RateLimit-Remaining: 0
RateLimit-Reset: 1730641030
6) Валидация входа: структура ошибок полей
Для 400/422 используйте массив ошибок полей:json
{
"type": "https://errors.example.com/validation",
"title": "Validation failed",
"status": 422,
"code": "VALIDATION_ERROR",
"trace_id": "01HX4...XYZ",
"errors": [
{"field": "amount", "rule": "min", "message": "Must be >= 10"},
{"field": "currency", "rule": "enum", "message": "Unsupported currency"}
]
}
7) Частичные неудачи (batch/partial failure)
В батч-эндпоинтах не прячьте ошибки внутри 200 без структуры. Возвращайте 207 Multi-Status или 200 c массивом результатов, где каждое задание имеет свой статус:json
{
"status": "partial",
"succeeded": 8,
"failed": 2,
"results": [
{"id": "op1", "status": 201},
{"id": "op2", "status": 422, "error": {"code":"VALIDATION_ERROR","detail":"..."}}
]
}
8) Пагинация и «пустые» ответы
Пустая коллекция — 200 с `items: []`, не 404.
Конец страницы — `next_page_token` отсутствует.
Некорректный токен — 400 с `code: PAGINATION_CURSOR_INVALID`.
9) Webhooks: надежная доставка
Подписывайте события (HMAC) и проверяйте до обработки.
Ответ на успешную обработку — 2xx (лучше 204).
Временные сбои получателя — 5xx; отправитель повторяет (экспоненциальный backoff, джиттер).
Дедупликация по `event_id` и сохранение результата (idempotent consumer).
Недопустимый payload — 400/422 без повторов.
10) Соответствия протоколам (gRPC/GraphQL)
gRPC → HTTP:- `INVALID_ARGUMENT` → 400
- `UNAUTHENTICATED` → 401
- `PERMISSION_DENIED` → 403
- `NOT_FOUND` → 404
- `ALREADY_EXISTS` → 409
- `FAILED_PRECONDITION` → 412/422
- `RESOURCE_EXHAUSTED` → 429
- `ABORTED` → 409
- `UNAVAILABLE` → 503
- `DEADLINE_EXCEEDED` → 504
json
{
"data": { "createPayment": null },
"errors": [{
"message": "Forbidden",
"extensions": { "code": "FORBIDDEN", "status": 403, "trace_id": "..." },
"path": ["createPayment"]
}]
}
Рекомендуется для критичных ошибок использовать не 200, а соответствующий HTTP-код.
11) Заголовки и подсказки клиенту
`Retry-After` — секунды/HTTP-дата (429/503/425/408).
`Warning` — мягкие деградации или депрекейт («199 - Feature X is deprecated»).
`Deprecation`, `Sunset`, `Link: <...>; rel="deprecation"` — для управляемого отключения.
`Problem-Type` (кастомный) — быстрый роутинг ошибок на клиенте.
`X-Trace-Id`/`Correlation-Id` — связывает логи/трейсы.
12) Безопасность сообщений
Не повторяйте входные секреты (токены/подписи) в теле ответа.
Маскируйте PAN/PII (`1234`).
Для 401/403 — не раскрывайте, какой именно атрибут провалился.
Для 404 вместо «resource exists but not yours» — просто 404.
13) Наблюдаемость ошибок
Метрики:- `http_errors_total{status, route, tenant}`
- `error_classes_total{code}` (по `code` из тела)
- доля 429, 5xx; `p95`/`p99` latency для ошибочных ответов отдельно
- `retry_after_seconds_bucket` — гистограмма советов по повторам
- связывайте ответ с `trace_id`, храните `code`, `type`, `status`, `route`, `tenant`, без PII.
- всплеск `5xx_rate > X%` при RPS>N;
- рост 429 на критичных маршрутах;
- `timeout/504` у зависимостей;
- частые 409/идемпотентность → признак гонок.
14) Примеры
14.1 422 (бизнес-валидация)
json
{
"type": "https://errors.example.com/payments/limit-exceeded",
"title": "Limit exceeded",
"status": 422,
"code": "PAYMENT_LIMIT_EXCEEDED",
"detail": "Daily withdrawal limit reached for KYC1.",
"hint": "Increase limits after KYC2 or try tomorrow.",
"trace_id": "01J5...XYZ"
}
14.2 409 (идемпотентность)
HTTP/1.1 409 Conflict
Idempotency-Replay: true
json
{
"type": "https://errors.example.com/idempotency/replay",
"title": "Duplicate request",
"status": 409,
"code": "IDEMPOTENT_REPLAY",
"detail": "A request with the same Idempotency-Key was already processed.",
"hint": "Reuse the same Idempotency-Key and GET the operation status."
}
14.3 429 (лимиты)
json
{
"type":"https://errors.example.com/rate/too-many-requests",
"title":"Too many requests",
"status":429,
"code":"RATE_LIMITED",
"detail":"Per-key rate limit exceeded.",
"hint":"Retry after the time specified in Retry-After header."
}
15) Антипаттерны
Возвращать 200 с текстом ошибки в теле.
Смешивать разные форматы ошибок между сервисами.
Раскрывать стек/SQL/имена таблиц/внутренние URL в `detail`.
Использовать `message` вместо стабильного `code`/`type`.
Возвращать 500 при ожидаемой бизнес-ошибке (например, «баланс недостаточен»).
Непоследовательная семантика между REST/GraphQL/gRPC.
16) Специфика iGaming/финансов
Четкие коды для KYC/AML/санкций: `KYC_REQUIRED`, `KYC_REVIEW`, `AML_LOCK`, `SANCTION_BLOCKED`.
Ограничения юрисдикций: 451 с безопасной формулировкой без указания списков.
Денежные write-операции: 409/423 при конкуренции и блокировках, `hint` с окном повтора.
Инварианты лимитов игрока: используйте 422 для нарушений правил ответственных платежей.
Аудит: неизменяемые журналы решений (код, время, актор, trace_id).
17) Чек-лист prod-готовности
- Единая JSON-схема ошибок, стабильные `type`/`code`.
- Маппинг HTTP ↔ gRPC/GraphQL согласован и задокументирован.
- Семантика ретраев + `Retry-After`; идемпотентность для write.
- Маскирование PII/секретов; 404 для скрытия ресурсов.
- Метрики ошибок и алерты; корелляция с `trace_id`.
- Политики депрекейта: `Deprecation`, `Sunset`, `Link`.
- Тесты: negative/fuzz, конфликт версий, падение зависимостей, double-submit.
- Гайд клиентам: примеры бэк-оффов и обработка 409/422/429/5xx.
18) TL;DR
Стандартизируйте единый JSON-формат ошибок c `type`/`code`/`trace_id`, используйте корректные HTTP-коды, различайте валидацию (400/422), права (401/403/404), конфликты/идемпотентность (409) и лимиты (429). Давайте четкие `Retry-After` и `hint`, маскируйте чувствительные данные, логируйте ошибки с `trace_id` и стройте алерты по 5xx/429/p99.