Обробка виключень ASP.NET Core Web API


280

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

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. Я очікував, що він буде схоплений так, як це працював із старим веб-API ASP.NET.

Тож як я можу зафіксувати всі винятки програм, а також будь-які винятки з Action Filters?


3
Ви пробували UseExceptionHandlerпроміжне програмне забезпечення?
Pawel

У мене є приклад тут про те , як використовувати UseExceptionHandlerпроміжне
Ілля Черномордик

Відповіді:


538

Виключення Обробка проміжного програмного забезпечення

Після багатьох експериментів з різними підходами до обробки винятків я закінчив використовувати проміжне програмне забезпечення. Це було найкращим чином для мого додатку ASP.NET Core Web API. Він обробляє винятки з додатків, а також винятки з фільтрів дій, і я маю повний контроль над обробкою винятків та HTTP-відповіддю. Ось мій виняток для обробки проміжного програмного забезпечення:

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 ex)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected

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

        var result = JsonConvert.SerializeObject(new { error = ex.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." }

Розглянемо ін'єкційного IOptions<MvcJsonOptions>до Invokeспособу потім використовувати його , коли ви серіалізовать об'єкт відповіді для використання параметрів сериализации ASP.NET MVC в JsonConvert.SerializeObject(errorObj, opts.Value.SerializerSettings)для кращої узгодженості сериализации в усіх кінцевих точках.

Підхід 2

Існує ще один неочевидний API, який називається UseExceptionHandler"нормально" для простих сценаріїв:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var feature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = feature.Error;

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

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


4
Я бився головою об стіл, намагаючись змусити власну проміжну програму працювати сьогодні, і це працює в основному так само (я використовую це для управління одиницею роботи / транзакції для запиту). Проблема, з якою я стикаюся, полягає в тому, що підвищені винятки в "next" не потрапляють в середнє програмне забезпечення. Як ви можете собі уявити, це проблематично. Що я роблю неправильно / пропав? Будь-які вказівки чи пропозиції?
brappleye3

5
@ brappleye3 - я зрозумів, у чому проблема. Я просто реєстрував проміжне програмне забезпечення в неправильному місці в класі Startup.cs. Я переїхав app.UseMiddleware<ErrorHandlingMiddleware>();лише до цього app.UseStaticFiles();. Виняток, здається, зараз спіймано правильно. Це змушує мене повірити, app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); app.UseBrowserLink();чи роблять якісь внутрішні магічні хакерські програми, щоб правильно впорядкувати програмне забезпечення.
Джамадан

4
Я погоджуюся, що користувацькі проміжні програми можуть бути дуже корисними, але ставлять під сумнів використання виключень для ситуацій NotFound, Neauthorized та BadRequest. Чому б просто не встановити код статусу (використовуючи NotFound () тощо), а потім обробити його у власному програмному посередництві або через UseStatusCodePagesWithReExecute? Дивіться devtrends.co.uk/blog/handling-errors-in-asp.net-core-web-api для отримання додаткової інформації
Пол Хілес

4
Це погано, оскільки він завжди серіалізується на JSON, повністю ігноруючи зміст переговорів.
Конрад

5
@Konrad дійсна точка. Тому я сказав, що з цього прикладу можна починати роботу, а не кінцевий результат. Для 99% API API JSON більш ніж достатньо. Якщо вам здається, що ця відповідь недостатньо хороша, не соромтесь робити свій внесок.
Андрій

60

Останні Asp.Net Core(принаймні від 2.2, можливо, раніше) мають вбудоване проміжне програмне забезпечення, що робить його трохи простіше порівняно з реалізацією у прийнятій відповіді:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;

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

Це мало би зробити те саме, лише трохи менше коду для написання.

Важливо: Не забудьте додати його до UseMvc(або UseRoutingв .Net Core 3), оскільки важливий порядок.


Чи підтримує він DI як аргумент для обробника, чи потрібно використовувати шаблон локатора служби в обробнику?
lp

32

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

Для вашого конкретного прикладу, ви можете використовувати один з розширень в проміжному 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 Exception Middleware

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

Якщо це не працює для ваших потреб, ви завжди можете прокатати свій власний компонент Middleware, переглянувши їх реалізацію ExceptionHandlerMiddleware та ElmMiddleware, щоб зрозуміти концепції побудови власних.

Важливо додати виключення, що обробляє проміжне програмне забезпечення, нижче проміжного програмного забезпечення StatusCodePages, але вище всіх інших компонентів середнього програмного забезпечення. Таким чином ваше середнє програмне забезпечення Exception захопить виняток, запише його в журнал, а потім дозволить запиту перейти до середнього програмного забезпечення StatusCodePage, що відобразить користувачу дружню сторінку помилки.


Ласкаво просимо. Я також надав посилання на приклад переопрацювання за замовчуванням UseStatusPages для кращих справ, які можуть краще відповідати вашому запиту.
Ешлі Лі

1
Зауважте, що Elm не зберігає журнали, і рекомендується використовувати Serilog або NLog для забезпечення серіалізації. Див. Журнали ELM зникають. Чи можемо ми зберегти його у файлі чи БД?
Майкл Фрейджім

2
Зараз посилання розірвано.
Матіас Ліккегор Лоренцен

@AshleyLee, я сумніваюся, що UseStatusCodePagesвикористовується у впровадженні сервісів Web API. Переглядів або HTML взагалі немає, лише відповіді JSON ...
Paul Michalik

23

Добре прийнята відповідь мені дуже допомогла, але я хотів передати HttpStatusCode в моєму середньому програмному забезпеченні для управління кодом статусу помилок під час виконання.

За цим посиланням у мене з’явилася якась ідея зробити те саме. Тому я з’єднав відповідь Андрія з цим. Отже, мій підсумковий код нижче:
1. Базовий клас

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}

2. Спеціальний тип класу виключень

 public class HttpStatusCodeException : Exception
{
    public HttpStatusCode StatusCode { get; set; }
    public string ContentType { get; set; } = @"text/plain";

    public HttpStatusCodeException(HttpStatusCode statusCode)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, string message) : base(message)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, Exception inner) : this(statusCode, inner.ToString()) { }

    public HttpStatusCodeException(HttpStatusCode statusCode, JObject errorObject) : this(statusCode, errorObject.ToString())
    {
        this.ContentType = @"application/json";
    }

}


3. Програмне забезпечення для спеціальних винятків

public class CustomExceptionMiddleware
    {
        private readonly RequestDelegate next;

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

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

    private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception)
    {
        string result = null;
        context.Response.ContentType = "application/json";
        if (exception is HttpStatusCodeException)
        {
            result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)exception.StatusCode }.ToString();
            context.Response.StatusCode = (int)exception.StatusCode;
        }
        else
        {
            result = new ErrorDetails() { Message = "Runtime Error", StatusCode = (int)HttpStatusCode.BadRequest }.ToString();
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        }
        return context.Response.WriteAsync(result);
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        string result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)HttpStatusCode.InternalServerError }.ToString();
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return context.Response.WriteAsync(result);
    }
}


4. Метод розширення

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
    {
        app.UseMiddleware<CustomExceptionMiddleware>();
    }

5. Налаштування методу в startup.cs

app.ConfigureCustomExceptionMiddleware();
app.UseMvc();

Тепер мій метод входу в контролер облікового запису:

 try
        {
            IRepository<UserMaster> obj = new Repository<UserMaster>(_objHeaderCapture, Constants.Tables.UserMaster);
            var Result = obj.Get().AsQueryable().Where(sb => sb.EmailId.ToLower() == objData.UserName.ToLower() && sb.Password == objData.Password.ToEncrypt() && sb.Status == (int)StatusType.Active).FirstOrDefault();
            if (Result != null)//User Found
                return Result;
            else// Not Found
                throw new HttpStatusCodeException(HttpStatusCode.NotFound, "Please check username or password");
        }
        catch (Exception ex)
        {
            throw ex;
        }

Вище ви можете побачити, чи не знайшов я користувача, піднявши HttpStatusCodeException, в якому я передав статус HttpStatusCode.NotFound та користувацьке повідомлення
в середньому програмному забезпеченні

улов (HttpStatusCodeException ex)

буде викликано заблокований, який передасть управління

приватний метод Tasle HandleExceptionAsync (контекст HttpContext, виняток HttpStatusCodeException)

.


Але що робити, якщо я раніше отримав помилку виконання? Для цього я використав спробу блок-лову, який викидає виняток і буде захоплений у блоці catch (виняток виключенняObj) і передасть управління на

Task HandleExceptionAsync (контекст HttpContext, виняток винятку)

метод.

Я використовував один клас ErrorDetails для однаковості.


Куди подіти метод розширення? На жаль, у startup.csв void Configure(IapplicationBuilder app)я отримую помилку IApplicationBuilder does not contain a definition for ConfigureCustomExceptionMiddleware. І я додав посилання, де CustomExceptionMiddleware.csє.
Spedo De La Rossa

ви не хочете використовувати винятки, оскільки вони сповільнюють ваш апіс. винятки дуже дорогі.
lnaie

@Inaie, Не можу про це сказати ... але, здається, ти ніколи не мав жодного винятку, щоб впоратися .. Чудова робота
Арджун

19

Щоб налаштувати поведінку поводження з винятками за типом винятку, ви можете використовувати Middleware з пакунків 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();
}

16

По-перше, завдяки Андрію, який я базував своє рішення на його прикладі.

Я включаю мою, оскільки це більш повний зразок і може заощадити читачів деякий час.

Обмеженням підходу Андрія є те, що він не обробляє журнал, фіксує потенційно корисні змінні запиту та узгодження контенту (він завжди повертатиме 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;
        }
    }
}

9

По-перше, налаштуйте ASP.NET Core 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....


0

використовувати проміжне програмне забезпечення або IExceptionHandlerPathFeature добре. є ще один спосіб у eshop

створіть фільтр винятку та зареєструйте його

public class HttpGlobalExceptionFilter : IExceptionFilter
{
  public void OnException(ExceptionContext context)
  {...}
}
services.AddMvc(options =>
{
  options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.