Как стать автором
Обновить
71.12
Рейтинг
Plarium
Разработчик мобильных и браузерных игр

Как обрабатывать необработанные исключения в ASP.NET Web API

Блог компании Plarium .NET *C# *ASP *Программирование *

Чтобы сделать обработку исключений простой и последовательной, старайтесь не бросать исключения в тех случаях, когда вы можете определить ошибку самостоятельно. Я рекомендую возвращать подходящую ошибку, как в коде ниже.

[HttpGet("{id:int}")]
public async Task<ActionResult<Order>> Get(int id, CancellationToken cancellationToken)
{
    var order = await _ordersService.Get(id, cancellationToken);
    if (order == null)
    {
        return NotFound();
    }

    return Ok(order);
}

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

Также избегайте такого состояния API, когда бросить исключение и отправить ошибку 500 — это единственный способ ответить на запрос. Такие ситуации должны стать поводом для рефакторинга дизайна вашего API и use cases. Отправляйте ошибку 500 только в исключительных необработанных случаях, таких как проблемы с базой данных, системные ошибки и т. п.

Существует несколько способов добавить обработку исключений в ASP.NET Core. Это Exception Filters, Exception handler lambda и Middleware. Я рекомендую последний. Middleware отлавливает ошибки из конструкторов контроллеров, фильтров и обработчиков, ошибки маршрутизации и т. п.

Реализуйте интерфейс IMiddleware и зарегистрируйте этот класс в Startup.cs, как в коде ниже. Обработчик ошибок должен быть первым в конвейере, чтобы ловить любые исключения при обработке запроса.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Middlewares
        services.AddTransient<ErrorHandlerMiddleware>();
        services.AddTransient<YourCustomMiddleware>();

        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseMiddleware<ErrorHandlerMiddleware>(); // Should be always in the first place

        app.UseRouting();
        app.UseCors();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseMiddleware<YourCustomMiddleware>();
        app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
    }
}

Если исключение не обработано, клиенты API получат Unknown Error. Простейший обработчик ошибок должен поймать исключение, залогировать его и отправить статус Internal Server Error. Код ниже добавляет C#-класс, который делает все перечисленное.

public class ErrorHandlerMiddleware : IMiddleware
{
    private readonly ILogger<ErrorHandlerMiddleware> _logger;

    public ErrorHandlerMiddleware(ILogger<ErrorHandlerMiddleware> logger)
    {
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        try
        {
            await next(context);
        }
        catch (Exception exception)
        {
            const string message = "An unhandled exception has occurred while executing the request.";
            _logger.LogError(exception, message);
            
            context.Response.Clear();
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        }
    }
}

Я уверен, что нет необходимости проверять свойство context.Response.HasStarted. Его достаточно хорошо обрабатывает сам .NET, бросая InvalidOperationException с подробным сообщением. Как это выглядит в консоли в нашем случае:

Достаточно ли этого? У меня в проектах обычно немного больше требований к обработке ошибок. Вот они:

  1. Логируйте больше деталей об исключении. Не добавляйте эти сведения в сообщение исключения. (О том, как использовать свойство Exception.Data, чтобы логировать дополнительные сведения об исключениях, вы можете прочитать в моей предыдущей статье.)

  2. Не отправляйте секретную внутреннюю информацию клиентам Web API, такую как stack trace, exception data и т. п.

  3. Не обрабатывайте TaskCanceledException как внутреннюю ошибку сервера, потому что таковой она не является: причина исключения в отмене запроса клиентом, так что наиболее подходящий HTTP-ответ — это 499.

  4. Используйте JSON как наиболее подходящий веб-формат для обработки ошибок на стороне клиента.

  5. Текст ошибок переводится на другие языки, поэтому лучше не показывать пользователю сообщение исключения. Это должно быть что-то, что можно легко перевести, например: «Ой! Что-то пошло не так». Также в сообщении должен быть какой-нибудь уникальный код, с которым пользователь может обратиться в службу поддержки вашего приложения.

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

Ниже пример более сложного обработчика ошибок, который соответствует этим требованиям.

public class ErrorHandlerMiddleware : IMiddleware
{
    private static readonly JsonSerializerOptions SerializerOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
        WriteIndented = true
    };

    private readonly IWebHostEnvironment _env;
    private readonly ILogger<ErrorHandlerMiddleware> _logger;

    public ErrorHandlerMiddleware(IWebHostEnvironment env, ILogger<ErrorHandlerMiddleware> logger)
    {
        _env = env;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        try
        {
            await next(context);
        }
        catch (Exception exception) when (context.RequestAborted.IsCancellationRequested)
        {
            const string message = "Request was cancelled";
            _logger.LogInformation(message);
            _logger.LogDebug(exception, message);

            context.Response.Clear();
            context.Response.StatusCode = 499; //Client Closed Request
        }
        catch (Exception exception)
        {
            exception.AddErrorCode();
            const string message = "An unhandled exception has occurred while executing the request.";
            _logger.LogError(exception, exception is YourAppException ? exception.Message : message);

            const string contentType = "application/json";
            context.Response.Clear();
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = contentType;

            var json = ToJson(exception);
            await context.Response.WriteAsync(json);
        }
    }

    private string ToJson(in Exception exception)
    {
        var message = exception.Message;
        var code = exception.GetErrorCode();
        if (!_env.IsDevelopmentOrQA())
        {
            return JsonSerializer.Serialize(new { message, code }, SerializerOptions);
        }

        try
        {
            var info = exception.ToString();
            var data = exception.Data;
            var error = new { message, code, info, data };
            return JsonSerializer.Serialize(error, SerializerOptions);
        }
        catch (Exception ex)
        {
            const string mes = "An exception has occurred while serializing error to JSON";
            _logger.LogError(ex, mes);
        }

        return string.Empty;
    }
}

Я предлагаю использовать хэш-код исключения как код ошибки, чтобы отправлять пользователям один и тот же код на схожие проблемы. Чтобы создать короткий код, подойдет любой хэш-алгоритм. Я применяю наиболее доступный SHA-1, затем обрезаю результат до длины, достаточной для того, чтобы сохранить уникальность кода ошибки. Расширение для класса Exception, создающее короткий код ошибки, добавляется с помощью кода ниже.

private const string ErrorCodeKey = "errorCode";

public static Exception AddErrorCode(this Exception exception)
{
    using var sha1 = SHA1.Create();
    var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(exception.ToString()));
    var errorCode = string.Concat(hash[..5].Select(b => b.ToString("x")));
    exception.Data[ErrorCodeKey] = errorCode;
    return exception;
}

public static string GetErrorCode(this Exception exception)
{
    return (string)exception.Data[ErrorCodeKey];
}

Простой пример всплывающего окна об ошибке на стороне клиента.

Я надеюсь, этот подход поможет вам в поддержке приложений. Буду благодарен за вопросы и комментарии к статье :)

Теги:
Хабы:
Рейтинг 0
Просмотры 31
Комментарии Комментировать

Информация

Дата основания
2009
Местоположение
Израиль
Сайт
company.plarium.com
Численность
1 001–5 000 человек
Дата регистрации