ASP.NET обработка исключений Core Web API

Я начал использовать ASP.NET ядро для моего нового проекта REST API после использования обычного ASP.NET Web API на протяжении многих лет. Я не вижу хорошего способа обрабатывать исключения в ASP.NET Core Web API. Я попытался реализовать фильтр обработки исключений/атрибут:

public class ErrorHandlingFilter : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        HandleExceptionAsync(context);
        context.ExceptionHandled = true;
    }

    private static void HandleExceptionAsync(ExceptionContext context)
    {
        var exception = context.Exception;

        if (exception is MyNotFoundException)
            SetExceptionResult(context, exception, HttpStatusCode.NotFound);
        else if (exception is MyUnauthorizedException)
            SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
        else if (exception is MyException)
            SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
        else
            SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
    }

    private static void SetExceptionResult(
        ExceptionContext context, 
        Exception exception, 
        HttpStatusCode code)
    {
        context.Result = new JsonResult(new ApiResponse(exception))
        {
            StatusCode = (int)code
        };
    }
}

и вот моя регистрация фильтра запуска:

services.AddMvc(options =>
{
    options.Filters.Add(new AuthorizationFilter());
    options.Filters.Add(new ErrorHandlingFilter());
});

проблема у меня была в том, что когда исключение произошло в моем AuthorizationFilter это не обрабатывается ErrorHandlingFilter. Я ожидал, что он будет пойман там просто как это работает со старыми ASP.NET Web API.

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

5 ответов


Промежуточное ПО Обработки Исключений

после многих экспериментов с различными подходами обработки исключений я закончил использование промежуточного программного обеспечения. Это сработало лучше всего для моего ASP.NET приложение Core Web API. Он обрабатывает исключения приложений, а также исключения из фильтров, и я полностью контролирую обработку исключений и создание ответа json. Вот мое промежуточное ПО обработки исключений:

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate next;

    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected

        if      (exception is MyNotFoundException)     code = HttpStatusCode.NotFound;
        else if (exception is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
        else if (exception is MyException)             code = HttpStatusCode.BadRequest;

        var result = JsonConvert.SerializeObject(new { error = exception.Message });
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;
        return context.Response.WriteAsync(result);
    }
}

зарегистрировать прежде чем в MVC на Startup класс:

app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();

вот пример ответа исключения:

{ "error": "Authentication token is not valid." }

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


лучше всего использовать промежуточное ПО для ведения журнала, который вы ищете. Вы хотите поместить журнал исключений в одно промежуточное ПО, а затем обработать страницы ошибок, отображаемые пользователю в другом промежуточном по. Это позволяет разделить логику и следует дизайн Microsoft выложил с 2 промежуточного программного обеспечения componenets. Вот хорошая ссылка на документацию Microsoft:обработка ошибок в ASP.Net ядро

для вашего конкретного примера вы можете использовать одно из расширений в промежуточное ПО StatusCodePage или свернуть свой собственный, как этой.

вы можете найти пример здесь для регистрации исключений:ExceptionHandlerMiddleware.cs

public void Configure(IApplicationBuilder app)
{
    // app.UseErrorPage(ErrorPageOptions.ShowAll);
    // app.UseStatusCodePages();
    // app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
    // app.UseStatusCodePages("text/plain", "Response, status code: {0}");
    // app.UseStatusCodePagesWithRedirects("~/errors/{0}");
    // app.UseStatusCodePagesWithRedirects("/base/errors/{0}");
    // app.UseStatusCodePages(builder => builder.UseWelcomePage());
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");  // I use this version

    // Exception handling logging below
    app.UseExceptionHandler();
}

Если вам не нравится эта конкретная реализация, то вы также можете использовать промежуточное ПО ELM и вот несколько примеров: Elm Исключение Промежуточного Программного Обеспечения

public void Configure(IApplicationBuilder app)
{
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");
    // Exception handling logging below
    app.UseElmCapture();
    app.UseElmPage();
}

Если это не работает для ваших нужд, вы всегда можете свернуть свой собственный компонент промежуточного ПО, посмотрев на их реализации ExceptionHandlerMiddleware и ElmMiddleware, чтобы понять концепции для создания собственного.

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


для настройки поведения обработки исключений для каждого типа исключений можно использовать промежуточное ПО из пакетов NuGet:

код:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddExceptionHandlingPolicies(options =>
    {
        options.For<InitializationException>().Rethrow();

        options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();

        options.For<SomeBadRequestException>()
        .Response(e => 400)
            .Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
            .WithBody((req,sw, exception) =>
                {
                    byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
                    return sw.WriteAsync(array, 0, array.Length);
                })
        .NextPolicy();

        // Ensure that all exception types are handled by adding handler for generic exception at the end.
        options.For<Exception>()
        .Log(lo =>
            {
                lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
                lo.Category = (context, exception) => "MyCategory";
            })
        .Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
            .ClearCacheHeaders()
            .WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
        .Handled();
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseExceptionHandlingPolicies();
    app.UseMvc();
}

во-первых, настроить ASP.NET ядро 2 Startup для повторного выполнения на странице ошибок для любых ошибок с веб-сервера и любых необработанных исключений.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment()) {
        // Debug config here...
    } else {
        app.UseStatusCodePagesWithReExecute("/Error");
        app.UseExceptionHandler("/Error");
    }
    // More config...
}

затем определите тип исключения, который позволит вам создавать ошибки с кодами состояния HTTP.

public class HttpException : Exception
{
    public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
    public HttpStatusCode StatusCode { get; private set; }
}

наконец, в контроллере для страницы ошибки настройте ответ на основе причины ошибки и того, будет ли ответ виден непосредственно конечным пользователем. Этот код предполагает, что все URL-адреса API начинаются с /api/.

[AllowAnonymous]
public IActionResult Error()
{
    // Gets the status code from the exception or web server.
    var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
        httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;

    // For API errors, responds with just the status code (no page).
    if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
        return StatusCode((int)statusCode);

    // Creates a view model for a user-friendly error page.
    string text = null;
    switch (statusCode) {
        case HttpStatusCode.NotFound: text = "Page not found."; break;
        // Add more as desired.
    }
    return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}

ASP.NET Core будет регистрировать сведения об ошибке для отладки, поэтому код состояния может быть всем, что вы хотите предоставить (потенциально ненадежному) запросчику. Если вы хотите показать больше информации, вы можете улучшить HttpException обеспечить его. Для ошибок API вы можете поместить информацию об ошибке в кодировке JSON в тело сообщения, заменив return StatusCode... С return Json....


во-первых, спасибо Андрею, поскольку я основал свое решение на его примере.

Я включаю свой, поскольку это более полный образец и может сэкономить читателям некоторое время.

ограничение подхода Андрея заключается в том, что он не обрабатывает ведение журнала, захват потенциально полезных переменных запроса и согласование контента (он всегда будет возвращать JSON независимо от того, что запросил клиент - XML / обычный текст и т. д.).

мой подход заключается в использовании ObjectResult, который позволяет мы используем функциональность, запеченную в MVC.

этот код также предотвращает кэширование ответа.

ответ на ошибку был оформлен таким образом, что он может быть сериализован XML-сериализатором.

public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate next;
    private readonly IActionResultExecutor<ObjectResult> executor;
    private readonly ILogger logger;
    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public ExceptionHandlerMiddleware(RequestDelegate next, IActionResultExecutor<ObjectResult> executor, ILoggerFactory loggerFactory)
    {
        this.next = next;
        this.executor = executor;
        logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, $"An unhandled exception has occurred while executing the request. Url: {context.Request.GetDisplayUrl()}. Request Data: " + GetRequestData(context));

            if (context.Response.HasStarted)
            {
                throw;
            }

            var routeData = context.GetRouteData() ?? new RouteData();

            ClearCacheHeaders(context.Response);

            var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

            var result = new ObjectResult(new ErrorResponse("Error processing request. Server error."))
            {
                StatusCode = (int) HttpStatusCode.InternalServerError,
            };

            await executor.ExecuteAsync(actionContext, result);
        }
    }

    private static string GetRequestData(HttpContext context)
    {
        var sb = new StringBuilder();

        if (context.Request.HasFormContentType && context.Request.Form.Any())
        {
            sb.Append("Form variables:");
            foreach (var x in context.Request.Form)
            {
                sb.AppendFormat("Key={0}, Value={1}<br/>", x.Key, x.Value);
            }
        }

        sb.AppendLine("Method: " + context.Request.Method);

        return sb.ToString();
    }

    private static void ClearCacheHeaders(HttpResponse response)
    {
        response.Headers[HeaderNames.CacheControl] = "no-cache";
        response.Headers[HeaderNames.Pragma] = "no-cache";
        response.Headers[HeaderNames.Expires] = "-1";
        response.Headers.Remove(HeaderNames.ETag);
    }

    [DataContract(Name= "ErrorResponse")]
    public class ErrorResponse
    {
        [DataMember(Name = "Message")]
        public string Message { get; set; }

        public ErrorResponse(string message)
        {
            Message = message;
        }
    }
}