Централизованная обработка ошибок. Метод расширения UseExceptionHandler()

Обработка исключений позволяет нам избежать аварийного завершения работы приложения. Для обработки исключений в приложениях C#, обычно, используются блоки try...catch с ключевым словом final для того, чтобы очистить ресурсы приложения. Это стандартная практика обработки исключений, которая может также применяться и в приложениях ASP.NET Core Web API. Однако, платформа ASP.NET Core позволяет извлечь всю логику обработки исключений и сосредоточить её в одном централизованном месте, избежав тем самым использования блоков try…catch в действиях контроллера и обеспечить, при необходимости, единый формат сообщений о возникших исключительных ситуациях в приложении. Такой подход к обработке исключений в приложениях ASP.NET Core называется Global Error Handling – глобальная обработка ошибок.

Метод расширения UseExceptionHandler()

Метод расширения UseExceptionHandler() добавляет в конвейер обработки запросов компоненты middleware для обработки исключений в приложении. Рассмотрим использование этого метода на примере одной из его версий:

public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, Action<IApplicationBuilder> configure)

Этот метод принимает в качестве параметра делегат Action<IApplicationBuilder>, используя который мы можем добавить в конвейер обработки запросов свой компонент middleware для централизованной обработки исключений в приложении.

Итак, создадим новое приложение ASP.NET Core Web API и воспользуемся методом UseExceptionHandler() следующим образом

using Microsoft.AspNetCore.Diagnostics;
using System.Net;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

var app = builder.Build();

app.UseExceptionHandler(configure => configure.Run(async context =>
{
    context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
    context.Response.ContentType = "application/json";

    var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
    if (contextFeature != null)
    {
        app.Logger.LogError($"Что-то случилось: {contextFeature.Error}");
        await context.Response.WriteAsync("Внутренняя ошибка сервера.");
    }
}));

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

Посмотрим на вызов метода UseExceptionHandler(). Здесь мы добавляем в конвейер обработки запросов терминальный компонент middleware:

configure.Run(async context =>
{
    context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
    context.Response.ContentType = "application/json";

    var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
    if (contextFeature != null)
    {
        app.Logger.LogError($"Что-то случилось: {contextFeature.Error}");
        await context.Response.WriteAsync("Внутренняя ошибка сервера.");
    }
})

В компоненте middleware мы устанавливаем код статуса HTTP равным 500 Internal Server Error (внутренняя ошибка сервера) и заголовок Content-Type равным «application/json». Далее мы, используя свойство Features контекста запроса запрашиваем объект, реализующий интерфейс IExceptionHandlerFeature

var contextFeature = context.Features.Get<IExceptionHandlerFeature>();

Этот объект предоставляет нам информацию об ошибке, которая возникла при выполнении запроса. Таким образом, если переменная contextFeature будет равна null, то это означает, что запрос выполнен успешно, иначе – при выполнении запроса произошла ошибка. В случае возникновения ошибки мы выводим сведения о ней в лог приложения, а также отправляем пользователю сообщение с текстом «Внутренняя ошибка сервера»

if (contextFeature != null)
{
    app.Logger.LogError($"Что-то случилось: {contextFeature.Error}");
    await context.Response.WriteAsync("Внутренняя ошибка сервера.");
}

Чтобы посмотреть то, как будут обрабатываться теперь ошибки в приложении специально создадим исключительную ситуацию в контроллере WeatherForecastController :

[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
    throw new NotImplementedException();

    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
    })
    .ToArray();
}

Теперь откроем http-файл проекта и выполним запрос на получение прогноза погоды:

Можно немного доработать код компонента middleware и организовать глобальную обработку ошибок таким образом, чтобы пользователю возвращался ответ одного формата – что при валидации модели, что при возникновении исключений, а также написать свой метод расширения для IApplicationBuilder с помощью которого мы будем добавлять обработку ошибок в наше приложение. Для этого добавим в проект новый класс под названием GlobalErrorHandlingExtensions 

public static class GlobalErrorHandlingExtensions
{
    public static void UseGlobalErrorHandling(this WebApplication app, IHostEnvironment environment, ILogger logger)
    {
        app.UseExceptionHandler(configure => configure.Run(async context =>
        {
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            context.Response.ContentType = "application/json";

            var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
            if (contextFeature != null)
            {
                logger.LogError($"Что-то случилось: {contextFeature.Error}");
                var problem = new ValidationProblemDetails
                {
                    Instance = context.Request.Path,
                    Status = (int)HttpStatusCode.InternalServerError,
                    Title = "Internal Server Error",
                    Type = "ServerError",
                };
                if (environment.IsDevelopment())
                {
                    problem.Detail = contextFeature.Error.ToString();
                    problem.Extensions["traceId"] = context.TraceIdentifier;
                }
                else
                {
                    problem.Detail = contextFeature.Error.Message;
                }

                var error = new Dictionary<string, string[]>();
                error.Add("global", [$"{contextFeature.Error}"]);
                problem.Errors = error;

                await context.Response.WriteAsJsonAsync(problem);
            }
        }));
    }
}

Первое, на что стоит обратить внимание – это на параметры метода расширения UseGlobalErrorHandling(). Здесь мы запрашиваем две зависимости – IHostEnvironment и ILogger. Первая зависимость используется для того, чтобы в зависимости от того, в какой среде запущено приложение, приложение формировало ответ сервера, который получит пользователь при возникновении исключения. Вторая зависимость (ILogger) используется нами для вывода сообщения об исключении в лог приложения. Само сообщение об ошибке в формате RFC 7807 формируется следующим образом:

var problem = new ValidationProblemDetails
{
    Instance = context.Request.Path,
    Status = (int)HttpStatusCode.InternalServerError,
    Title = "Internal Server Error",
    Type = "ServerError",
};
if (environment.IsDevelopment())
{
    problem.Detail = contextFeature.Error.ToString();
    problem.Extensions["traceId"] = context.TraceIdentifier;
}
else
{
    problem.Detail = contextFeature.Error.Message;
}

var error = new Dictionary<string, string[]>
{
    { "global", [$"{contextFeature.Error}"] }
};
problem.Errors = error;

Здесь стоит обратить внимание на формирование значения свойства Detail. Чтобы определить состав информации, которая будет передана пользователю, мы проверяем название среды окружения. Если среда окружения «Development», то мы передаем наиболее полную информацию об ошибке, то есть, по сути, всё то, что выводится в лог приложения. Если же среда выполнения, например «Production», то в качестве значения свойства Detail мы отдаем пользователю только сообщение об ошибке, то есть мы уже не пересылаем пользователю конфиденциальную информацию об ошибке.

Теперь используем этот метод расширения для построения конвейера обработки запросов:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.UseGlobalErrorHandling(app.Environment, app.Logger);

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Так как расположение компонентов middleware в конвейере обработки запросов имеет значение, то стоит обратить внимание, что компонент для обработки ошибок стоит располагать как можно ближе к началу конвейера, что мы, собственно и сделали, вызвав метод UseGlobalErrorHandling() сразу после строки

var app = builder.Build();

Для начала запустим приложение из Visual Studio и выполним запрос, представленный выше. Так в этом случае имя среды будет «Development», то мы увидим следующий ответ сервера

Если же мы запустим приложение из проводника Windows и попытаемся выполнить тот же запрос, то получим сообщение об ошибке следующего вида:

Здесь стоит отметить, что в списке ошибок errors, в качестве примера, мы всё также выдаем полную информацию об исключении (различается только поле detail), однако в реальных приложениях Web API стоит избегать такой выдачи пользователю информации об исключении, ограничиваясь только необходимым набором сведений о возникшей проблеме.

Подобным образом можно организовать централизованную обработку ошибок в приложения, используя свою собственную логику.

Итого

Для того, чтобы приложение ASP.NET Core Web API обрабатывало все исключительные ситуации, возникающие при запросах пользователей, в одном централизованном месте мы можем использовать метод расширения UseExceptionHandler(), определив в его параметрах терминальный компонент middleware для выдачи сообщения об ошибке пользователю. Вызов UseExceptionHandler() должен находиться как можно ближе к началу конвейера обработки запросов.

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