Содержание
Начиная с ASP.NET Core 8.0 также для перехвата необработанных исключений и их централизованной обработки можно использовать интерфейс IExceptionHandler, который используется компонентом middleware для обработки исключений
Интерфейс IExceptionHandler
Интерфейс IExceptionHandler
предоставляет нам всего один метод:
public interface IExceptionHandler { ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken); }
Этот метод пытается асинхронно обработать указанное в exception
исключение в конвейере запросов. Если исключение обработано успешно, то метод должен вернуть значение true
, в противном случае – false
. При этом, если метод TryHandleAsync()
возвращает false
, то ошибка передается для обработки в следующий обработчик. Сам же обработчик регистрируется в контейнере зависимостей с использованием метода AddExceptionHandler<T>()
.
Давайте попробуем использовать этот интерфейс приложении ASP.NET Core Web API. Для этого создадим новое приложение и добавим в него класс с названием GlobalErrorHandler
public class GlobalErrorHandler : IExceptionHandler { private readonly ILogger _logger; private readonly IHostEnvironment _environment; public GlobalErrorHandler(IHostEnvironment environment, ILogger<GlobalErrorHandler> logger) { _environment = environment; _logger = logger; } public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken) { httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; httpContext.Response.ContentType = "application/json"; _logger.LogError($"Что-то случилось: {exception}"); var problem = new ProblemDetails { Instance = httpContext.Request.Path, Status = (int)HttpStatusCode.InternalServerError, Title = "Internal Server Error", Type = "ServerError", }; if (_environment.IsDevelopment()) { problem.Detail = exception.ToString(); problem.Extensions["traceId"] = httpContext.TraceIdentifier; } else { problem.Detail = exception.Message; } await httpContext.Response.WriteAsJsonAsync(problem, cancellationToken: cancellationToken); return true; } }
По сути, в методе TryHandleAsync()
мы выполняем похожие операции, что и в ранее разработанном нами компоненте middleware – только формируем объект типа ProblemDetails
для отправки его серверу, а не ValidationProblemDetails
, но сути это не меняет. Правда, при этом, нам уже не требуется запрашивать из коллекции Features
контекста запроса необходимый компонент – исключение мы получаем непосредственно из параметров метода. При этом метод всегда возвращает true
, сигнализируя тем самым, что исключение обработано.
Теперь нам необходимо зарегистрировать этот класс в контейнере зависимостей. Здесь стоит отметить, что при использовании IExceptionHandler
для настройки обработчика исключений ASP.NET Core также требует зарегистрировать сервисы для создания объектов ProblemDetails
(вызвать метод расширения IServiceCollection
AddProblemDetails()
), поэтому в Program.cs мы должны выполнить следующие изменения:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddProblemDetails(); builder.Services.AddExceptionHandler<GlobalErrorHandler>(); builder.Services.AddControllers(); var app = builder.Build(); app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
Теперь можно снова добавить в контроллер WeatherForecastController
вызов исключения:
[HttpGet] public IEnumerable<WeatherForecast> Get() { throw new NotImplementedException(); }
и выполнить запрос, получив результат централизованной обработки ошибок:
На первый взгляд, работа IExceptionHandler
ничем не отличается от уже известного нам метода расширения UseExceptionHandler()
— мы также формируем и отправляем ответ пользователю. Но различие есть и заключается оно в том, что используя несколько объектов, реализующих IExceptionHandler
мы можем выстроить, фактически, конвейер обработки исключений. Допустим, что мы хотим обрабатывать ошибки, например деления на ноль каким-то своим, особенным образом, а все прочие ошибки – представленным выше способом. С использованием IExceptionHandler
это сделать достаточно просто – напишем свой обработчик ошибки деления на ноль
public class DivisionByZeroHandler : IExceptionHandler { private readonly ILogger<DivisionByZeroHandler> _logger; public DivisionByZeroHandler(IHostEnvironment environment, ILogger<DivisionByZeroHandler> logger) { _logger = logger; } public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken) { if (exception.GetType() == typeof(DivideByZeroException)) { httpContext.Response.StatusCode = 501; _logger.LogError($"Обнаружено деление на ноль при попытке выполнить запрос {httpContext.Request.Path}"); var problem = new ProblemDetails() { Status = httpContext.Response.StatusCode, Title = "Деление на ноль", Instance = httpContext.Request.Path }; await httpContext.Response.WriteAsJsonAsync(problem, cancellationToken: cancellationToken); return true; } return false; } }
Код метода TryHandleAsync()
этого обработчика, помимо того, что он немного короче, чем в GlobalErrorHandler
также может возвращать как true
, так и false
, в зависимости от типа исключения, которое передается в параметре exception
. Теперь у нас в приложении два обработчика:
DivideByZeroException
– должен обрабатывать только исключения типаDivideByZeroException
GlobalErrorHandler
– должен обрабатывать все прочие исключения.
Чтобы организовать такую логику работы приложения, более конкретный обработчик исключений должен регистрироваться раньше в контейнере зависимостей, так как ASP.NET Core будет вызывать обработчики по порядку. Таким образом, регистрация обработчиков в Program.cs должна выглядеть следующим образом:
builder.Services.AddProblemDetails(); builder.Services.AddExceptionHandler<DivisionByZeroHandler>(); builder.Services.AddExceptionHandler<GlobalErrorHandler>();
Обработчик DivisionByZeroHandler
регистрируется перед GlobalErrorHandler
. Теперь, если где-то в приложении происходит ошибка деления на ноль, то вне зависимости от имени среды выполнения мы получим один и тот же объект JSON с описанием ошибки. Если же возникнет другая ошибка, то она всё также будет обрабатываться в GlobalErrorHandler
.
Итого
Таким образом, используя различные обработчики IExceptionHandler
, мы можем организовать централизованную обработку различных ошибок, фактически, выстраивая, если можно так выразиться, свой собственный конвейер обработки ошибок в приложении.