Содержание
Начиная с 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– должен обрабатывать только исключения типаDivideByZeroExceptionGlobalErrorHandler– должен обрабатывать все прочие исключения.
Чтобы организовать такую логику работы приложения, более конкретный обработчик исключений должен регистрироваться раньше в контейнере зависимостей, так как ASP.NET Core будет вызывать обработчики по порядку. Таким образом, регистрация обработчиков в Program.cs должна выглядеть следующим образом:
builder.Services.AddProblemDetails(); builder.Services.AddExceptionHandler<DivisionByZeroHandler>(); builder.Services.AddExceptionHandler<GlobalErrorHandler>();
Обработчик DivisionByZeroHandler регистрируется перед GlobalErrorHandler. Теперь, если где-то в приложении происходит ошибка деления на ноль, то вне зависимости от имени среды выполнения мы получим один и тот же объект JSON с описанием ошибки. Если же возникнет другая ошибка, то она всё также будет обрабатываться в GlobalErrorHandler.
Итого
Таким образом, используя различные обработчики IExceptionHandler, мы можем организовать централизованную обработку различных ошибок, фактически, выстраивая, если можно так выразиться, свой собственный конвейер обработки ошибок в приложении.
