Обработка ошибок и health checks
Обработка ошибок и наблюдаемость: единый формат ошибок, health checks, готовность к проду.
Суть: в проде нельзя показывать пользователю стектрейсы и нельзя «молча падать». Нужны централизованная обработка ошибок (единый формат ответа), логирование и health checks — точки, по которым инфраструктура проверяет, жив ли сервис.
Учебное приложение «работает на моей машине», боевое — должно достойно вести себя при сбоях: вернуть понятную ошибку клиенту, записать детали в лог для разработчика и сообщить системе мониторинга о своём состоянии.
Централизованная обработка ошибок
// единый обработчик: ловит исключения из всего конвейера
app.UseExceptionHandler(errApp =>
{
errApp.Run(async context =>
{
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new
{
error = "Внутренняя ошибка сервера" // без деталей наружу
});
});
});
Этот middleware стоит первым — он ловит исключения из всех нижестоящих, логирует их и возвращает аккуратный JSON вместо стектрейса. В .NET 8+ есть и IExceptionHandler для более структурированного подхода.
Health checks
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>(); // жива ли БД?
// ...
app.MapHealthChecks("/health");
Эндпоинт /health отвечает, в порядке ли сервис (и его зависимости — БД, кэш). Оркестраторы (Kubernetes, балансировщики) дёргают его, чтобы решить, слать ли трафик на инстанс.
Картина прод-обработки
Исключение в эндпоинте
|
v
[UseExceptionHandler]
|-- лог с деталями -> система логов (для разработчика)
+-- ответ клиенту: 500 { "error": "..." } (без стектрейса)
GET /health -> [HealthChecks] -> проверка БД и т.п.
все ок -> 200 Healthy
проблема -> 503 Unhealthy (балансировщик уберёт инстанс)
Как работает под капотом
UseExceptionHandler оборачивает конвейер в try/catch: при необработанном исключении он перехватывает его, очищает начатый ответ и передаёт управление вашему обработчику. Так клиент не видит внутренностей, а вы получаете лог. Health checks — это набор зарегистрированных проверок; эндпоинт /health прогоняет их и агрегирует результат в статус Healthy/Degraded/Unhealthy с соответствующим HTTP-кодом.
Частые ошибки
- Показывать стектрейс в проде. Это утечка информации и помощь атакующему. Детали — только в логи.
- Глотать исключения без логов. Пустой
catchпрячет проблему — её невозможно расследовать. - Нет health-эндпоинта. Без него оркестратор не знает, что инстанс сломался, и шлёт на него трафик.
Best practices
- Единый формат ошибок (например ProblemDetails) — клиентам проще обрабатывать.
- Разные детали по окружению: подробности в Development, минимум — в Production.
- Добавляйте health checks для критичных зависимостей (БД, очереди) и отдавайте корректный 503 при проблемах.
Централизованная обработка против разбросанных try/catch
Соблазн обернуть каждый метод в try/catch ведёт к дублированию и непоследовательности: где-то ошибку проглотили, где-то вернули стектрейс, где-то забыли залогировать. Правильный подход — централизованная обработка: одно middleware (UseExceptionHandler или IExceptionHandler в .NET 8+) перехватывает все необработанные исключения из конвейера, логирует детали и возвращает клиенту аккуратный единообразный ответ. Бизнес-код тогда занимается логикой, а не рутиной обработки ошибок, и формат ответов гарантированно одинаков по всему API.
Связка с правильными статус-кодами важна: ожидаемые ситуации («не найдено», «конфликт», «нет прав») возвращают как 404/409/403 явно из методов — это не ошибки сервера. А вот непредвиденные исключения превращаются в 500 централизованно, с записью в лог и без раскрытия внутренностей. Формат ProblemDetails делает даже ошибки предсказуемыми для клиента: один разбор на все случаи.
Наблюдаемость: логи, health checks, readiness против liveness
Боевое приложение должно быть наблюдаемым — по нему видно, что происходит. Это три кита: структурированные логи (что случилось и в каком контексте), метрики (сколько запросов, какие задержки, частота ошибок), трассировки (путь запроса через сервисы). Логи с корреляционными id позволяют собрать историю одного запроса целиком, а в проде детали ошибок уходят только в логи — клиент стектрейс не видит, чтобы не раскрывать структуру системы потенциальному атакующему.
Health checks бывают двух видов, и их различают намеренно. Liveness отвечает на вопрос «процесс жив?» — если нет, оркестратор перезапустит контейнер. Readiness отвечает «готов принимать трафик?» — проверяет зависимости (БД, кэш, очереди); если БД недоступна, инстанс временно убирают из балансировки, не убивая процесс. Эндпоинт вроде /health прогоняет зарегистрированные проверки и отдаёт 200 Healthy или 503 Unhealthy, а инфраструктура по этому коду принимает решение. Без health checks оркестратор шлёт трафик на сломанный инстанс вслепую. Этим уроком курс замкнул полный круг: от первого эндпоинта до сервиса, который не только работает, но и достойно ведёт себя при сбоях и понятен в эксплуатации.
Итог: зрелое приложение централизованно обрабатывает ошибки, прячет детали от клиента, логирует их и отдаёт health-статус. На этом курс по ASP.NET Core завершён — у вас есть карта от первого эндпоинта до прод-готового сервиса.