Централизованная обработка ошибок. Интерфейс IExceptionHandler

Начиная с 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. Теперь у нас в приложении два обработчика:

  1. DivideByZeroException – должен обрабатывать только исключения типа DivideByZeroException
  2. GlobalErrorHandler – должен обрабатывать все прочие исключения.

Чтобы организовать такую логику работы приложения, более конкретный обработчик исключений должен регистрироваться раньше в контейнере зависимостей, так как ASP.NET Core будет вызывать обработчики по порядку. Таким образом, регистрация обработчиков в Program.cs должна выглядеть следующим образом:

builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<DivisionByZeroHandler>();
builder.Services.AddExceptionHandler<GlobalErrorHandler>();

Обработчик DivisionByZeroHandler регистрируется перед GlobalErrorHandler. Теперь, если где-то в приложении происходит ошибка деления на ноль, то вне зависимости от имени среды выполнения мы получим один и тот же объект JSON с описанием ошибки. Если же возникнет другая ошибка, то она всё также будет обрабатываться в GlobalErrorHandler.

Итого

Таким образом, используя различные обработчики IExceptionHandler, мы можем организовать централизованную обработку различных ошибок, фактически, выстраивая, если можно так выразиться, свой собственный конвейер обработки ошибок в приложении.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии