Правильная обработка ошибок в веб-API — тот аспект разработки, который часто получает меньше внимания, чем заслуживает. В отличие от функциональных требований, ошибки сложно формализовать, их поведение редко попадает в JIRA-таски, а нюансы реализации часто остаются на усмотрение разработчиков. Между тем, продуманная стратегия обработки сбоев напрямую влияет на:
- Стабильность системы при частичных отказах
- Скорость дебага в production
- Качество DX (developer experience) для потребителей API
- Безопасность (информационное излучение через сообщения об ошибках)
Рассмотрим практические подходы к построению отказоустойчивых API с примерами для Node.js и Python.
Проектирование консистентной схемы ошибок
Первое правило: клиент должен уметь программно обрабатывать ошибки. Это значит, что все ответы API — включая ошибки — должны следовать строгому контракту. Стандартизация начинается с выбора подходящих HTTP-статусов:
// Плохо: 200 OK на ошибку
HTTP/1.1 200 OK
{
"success": false,
"error": "Invalid email format"
}
// Хорошо: Соответствие кода и содержимого
HTTP/1.1 422 Unprocessable Entity
{
"type": "validation_error",
"title": "Invalid input",
"detail": "Email must be a valid address",
"validation_errors": [
{
"field": "email",
"code": "invalid_format"
}
]
}
Для машиночитаемых деталей используйте RFC 7807 Problem Details, дополняя специфичными для домена полями.
Трехуровневая обработка на бэкенде
- Ожидаемые ошибки бизнес-логики
Перехватывайте исключения в сервисном слое:
class UserService {
async createUser(payload) {
const existing = await User.findOne({ email: payload.email });
if (existing) {
throw new ConflictError('Email already registered');
}
// ...
}
}
- Транспортный слой (middleware/интерцепторы)
Централизованный обработчик преобразует исключения в HTTP-ответы:
# Flask example
@app.errorhandler(APIError)
def handle_api_error(e):
response = jsonify({
"type": e.error_type,
"title": e.title,
"detail": e.detail,
"instance": request.path
})
response.status_code = e.status_code
return response
- Неожиданные ошибки
Глобальный обработчик для непредвиденных исключений — но никогда не раскрывайте стектрейс в production:
process.on('unhandledRejection', (reason, promise) => {
logger.fatal({ reason, promise }, 'Unhandled rejection');
// Плавный перезапуск лучше чем process.exit()
terminate(1, 'unhandledRejection');
});
Проброс контекста на фронтенд
Проблема фронтенд-разработчиков: получение из API ошибки вида "Something went wrong"
. Решение — маппинг кодов ошибок на понятные UI-действия:
// Типизированная обработка в Axios interceptor
interface APIError {
type: 'auth' | 'validation' | 'internal';
code: string;
detail?: string;
}
axios.interceptors.response.use(null, (error) => {
const apiError = error.response?.data as APIError;
if (apiError?.type === 'validation') {
showToast(transformValidationErrors(apiError));
return Promise.reject(apiError);
}
if (error.response?.status === 401) {
redirectToLogin();
return Promise.resolve(); // Прерываем цепочку ошибок
}
// Дефолтная обработка
captureException(error);
showGenericError();
return Promise.reject(error);
});
Логирование с возможностью троттлинга
Бесполезное логгирование выглядит так:
ERROR: User not found
Полезное логгирование включает контекст:
{
"severity": "WARN",
"type": "auth.failed",
"userId": "u_abc123",
"ip": "192.0.2.1",
"timestamp": "2023-10-05T12:34:56Z",
"metadata": {
"reason": "invalid_credentials",
"loginAttempts": 3
}
}
Инструменты:
- Winston/Bunyan + Loki для Node.js
- Structlog + ELK для Python
- Sentry/Datadog для кросс-стека
Важный нюанс: настройте sampling для высоконагруженных систем, чтобы не перегрузить хранилища логов.
Рекомендации по эволюции системы
- Ведите реестр кодов ошибок как документацию.
- Тестируйте обработчики ошибок как основной функционал — включая стресс-тесты с Chaos Monkey.
- Для GraphQL API используйте расширения к спецификации, аналогичные Apollo Error Codes.
- В gRAPI передавайте статусы через gRPC-коды и metadata.
Ошибки — не аномалии, а неотъемлемая часть потока данных в распределенных системах. Инвестиции в их обработку окупаются уменьшением MTTR (mean time to repair) и повышением общей устойчивости системы. Главное правило: делайте ошибки предсказуемыми для клиентов и информативными для разработчиков, но безопасными для конечных пользователей.