Ошибки в API как слепые зоны системы: они неизбежны, но профессиональная обработка превращает их из угрозы в источник информации. Рассмотрим комплексный подход, который выходит за рамки элементарных try-catch блоков.
Семантика ошибок как договор
HTTP статус-коды – это лингва франка между клиентом и сервером, но многие разработчики используют их непоследовательно. Основная путаница возникает между 4xx и 5xx кодами:
- 400 Bad Request – синтаксические ошибки (невалидный JSON, отсутствующие обязательные поля)
- 422 Unprocessable Entity – семантические ошибки (некорректный email, отрицательный возраст)
- 429 Too Many Requests – проблемы с rate limiting
- 503 Service Unavailable – явное указание на временную недоступность сервиса
Пример плохой практики:
# Неправильно: смешивание проверок валидации и бизнес-логики
if not request.json.get('email'):
abort(500, 'Email required')
Правильный подход с FastAPI:
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
email: EmailStr
password: str = Field(min_length=8)
@app.post("/users")
async def create_user(user: UserCreate):
# Валидация обрабатывается автоматически
Согласованная структура ошибок
Стандартизированный формат ответов критичен для DX (Developer Experience). Пример структуры с контекстной информацией:
{
"error": {
"code": "invalid_payment_method",
"message": "Card declined: insufficient funds",
"details": {
"retry_allowed": true,
"next_attempt": "2024-03-15T14:00:00Z",
"documentation_url": "https://api.example.com/docs/errors#invalid_payment_method"
},
"trace_id": "abc123-x5d8f"
}
}
Ключевые элементы:
- Машинно-читаемый код ошибки
- Локализованное сообщение (определяется через заголовок Accept-Language)
- Динамические метаданные для клиентской логики
- Корреляционный идентификатор для логирования
Паттерны промышленного логирования
Централизованный error handler – обязательный компонент зрелой API-инфраструктуры. Пример для Express.js:
app.use((err, req, res, next) => {
const traceId = generateCorrelationId();
logger.error({
traceId,
path: req.path,
params: req.params,
user: req.user?.id,
error: {
message: err.message,
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
}
});
res.status(err.status || 500).json({
error: {
code: err.code || 'internal_error',
message: err.expose ? err.message : 'Internal Server Error',
trace_id: traceId
}
});
});
Важные аспекты:
- Чувствительные данные маскируются перед логированием
- Разделение сообщений для разработчиков и конечных пользователей
- Контекст запроса сохраняется для последующего анализа
Клиентская сторона уравнения
Элегантная обработка на клиенте требует стратегии, а не разрозненных проверок. Пример для React с аксиос:
const api = axios.create({
timeout: 10000,
validateStatus: (status) => status < 500
});
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.data?.error?.code === 'rate_limited') {
const retryAfter = error.response.headers['retry-after'];
queueMicrotask(() => retryRequest(error.config, retryAfter));
return Promise.reject(error);
}
if (isNetworkError(error)) {
showGlobalOfflineWarning();
}
throw error.response ? new ApiError(error.response.data.error) : error;
}
);
class ApiError extends Error {
constructor({ code, message, details }) {
super(message);
this.code = code;
this.details = details;
}
}
Продвинутые техники
Идемпотентность для надежных повторных попыток:
POST /payments
X-Idempotency-Key: 7fa2158f-354c-48f3-9d6d-3e872e1e2c7d
Бэкендовая реализация с Redis:
def process_payment(request):
idempotency_key = request.headers.get('X-Idempotency-Key')
if idempotency_key:
with redis.lock(f'idempotency:{idempotency_key}', timeout=10):
if redis.exists(idempotency_key):
return redis.get(idempotency_key)
result = execute_payment(request)
redis.setex(idempotency_key, 24*3600, result)
return result
Динамическое управление ошибками через feature flags:
error_handling:
enable_stacktrace: ${FEATURE_STACKTRACE:-false}
expose_details:
- service: payment
environment: staging
level: debug
- service: analytics
environment: prod
level: basic
Обратная совместимость и версионирование
При изменениях в error payloads сохраняйте старые поля как устаревшие:
{
"error": {
"legacy_code": 4031,
"new_code": "geo_restricted",
"message": "Service unavailable in your region",
"deprecation_warning": "legacy_code will be removed after 2025-01-01"
}
}
Практические правила эволюции ошибок:
- Добавлять новые поля, но не удалять существующие
- Использовать заголовок Sunset для устаревающих кодов
- Поддерживать версионные неймспейсы (/v1/errors)
Современная обработка ошибок – это не просто техническая необходимость, а стратегический элемент дизайна API. Она напрямую влияет на надежность системы, скорость отладки и общие издержки поддержки. Инвестиции в продуманную инфраструктуру ошибок окупаются при первых серьезных инцидентах в production.