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

280

Я использую ASP.NET Core для моего нового проекта API REST после использования обычного веб-API ASP.NET в течение многих лет. Я не вижу хорошего способа обработки исключений в 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.

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

Андрей
источник
3
Вы пробовали UseExceptionHandlerпромежуточное ПО?
Павел
У меня есть пример здесь о том , как использовать UseExceptionHandlerпромежуточное
Илья Черномордик

Ответы:

539

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

После многих экспериментов с различными подходами к обработке исключений я в конечном итоге использовал промежуточное ПО. Лучше всего сработало для моего приложения 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, Unauthorized и BadRequest. Почему бы просто не установить код состояния (используя NotFound () и т. Д.), А затем обработать его в своем пользовательском промежуточном программном обеспечении или через UseStatusCodePagesWithReExecute? См. Devtrends.co.uk/blog/handling-errors-in-asp.net-core-web-api для получения дополнительной информации
Пол
4
Это плохо, потому что он всегда сериализуется в JSON, полностью игнорируя согласование контента.
Конрад
5
@ Конрад действительная точка. Вот почему я сказал, что в этом примере вы можете начать, а не конечный результат. Для 99% API JSON более чем достаточно. Если вы чувствуете, что этот ответ недостаточно хорош, не стесняйтесь вносить свой вклад.
Андрей
61

Последний 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
33

Лучше всего использовать промежуточное программное обеспечение для ведения журнала, который вы ищете. Вы хотите поместить журнал исключений в одно промежуточное ПО, а затем обрабатывать отображаемые пользователю страницы ошибок в другом промежуточном ПО. Это позволяет разделить логику и следовать дизайну, который 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 Middleware , и вот несколько примеров: 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 ...
Пол Михалик
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 и пользовательское сообщение
в промежуточном ПО.

catch (HttpStatusCodeException ex)

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

метод приватной задачи HandleExceptionAsync (контекст HttpContext, исключение HttpStatusCodeException)

,


Но что, если я получил ошибку во время выполнения раньше? Для этого я использовал блок try catch, который генерирует исключение и будет перехвачен в блоке catch (Exception exceptionObj) и передаст управление

Task HandleExceptionAsync (контекст HttpContext, исключение исключения)

метод.

Я использовал один класс ErrorDetails для единообразия.

Арджун
источник
Где поставить метод расширения? К сожалению, в startup.csв void Configure(IapplicationBuilder app)я получаю ошибку IApplicationBuilder does not contain a definition for ConfigureCustomExceptionMiddleware. И я добавил ссылку, где CustomExceptionMiddleware.csнаходится.
Spedo De La Rossa
Вы не хотите использовать исключения, так как они замедляют ваш apis. исключения очень дорогие.
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;
        }
    }
}
CountZero
источник
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 - это нормально. в интернет- магазине есть другой путь

создать исключительный фильтр и зарегистрировать его

public class HttpGlobalExceptionFilter : IExceptionFilter
{
  public void OnException(ExceptionContext context)
  {...}
}
services.AddMvc(options =>
{
  options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})
ws_
источник