Web api обработка ошибок

Время на прочтение
6 мин

Количество просмотров 10K

В преддверии старта курса «C# ASP.NET Core разработчик» подготовили традиционный перевод полезного материала.

Также рекомендуем посмотреть вебинар на тему

«Отличия структурных шаблонов проектирования на примерах». На этом открытом уроке участники вместе с преподавателем-экспертом познакомятся с тремя структурными шаблонами проектирования: Заместитель, Адаптер и Декоратор.


Введение 

Сегодня в этой статье мы обсудим концепцию обработки исключений в приложениях ASP.NET Core. Обработка исключений (exception handling) — одна из наиболее важных импортируемых функций или частей любого типа приложений, которой всегда следует уделять внимание и правильно реализовывать. Исключения — это в основном средства ориентированные на обработку рантайм ошибок, которые возникают во время выполнения приложения. Если этот тип ошибок не обрабатывать должным образом, то приложение будет остановлено в результате их появления.

В ASP.NET Core концепция обработки исключений подверглась некоторым изменениям, и теперь она, если можно так сказать, находится в гораздо лучшей форме для внедрения обработки исключений. Для любых API-проектов реализация обработки исключений для каждого действия будет отнимать довольно много времени и дополнительных усилий. Но мы можем реализовать глобальный обработчик исключений (Global Exception handler), который будет перехватывать все типы необработанных исключений. Преимущество реализации глобального обработчика исключений состоит в том, что нам нужно определить его всего лишь в одном месте. Через этот обработчик будет обрабатываться любое исключение, возникающее в нашем приложении, даже если мы объявляем новые методы или контроллеры. Итак, в этой статье мы обсудим, как реализовать глобальную обработку исключений в ASP.NET Core Web API.

Создание проекта ASP.NET Core Web API в Visual Studio 2019

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

  • Откройте Microsoft Visual Studio и нажмите «Create a New Project» (Создать новый проект).

  • В диалоговом окне «Create New Project» выберите «ASP.NET Core Web Application for C#» (Веб-приложение ASP.NET Core на C#) и нажмите кнопку «Next» (Далее).

  • В окне «Configure your new project» (Настроить новый проект) укажите имя проекта и нажмите кнопку «Create» (Создать).

  • В диалоговом окне «Create a New ASP.NET Core Web Application» (Создание нового веб-приложения ASP.NET Core) выберите «API» и нажмите кнопку «Create».

  • Убедитесь, что флажки «Enable Docker Support» (Включить поддержку Docker) и «Configure for HTTPS» (Настроить под HTTPS) сняты. Мы не будем использовать эти функции.

  • Убедитесь, что выбрано «No Authentication» (Без аутентификации), поскольку мы также не будем использовать аутентификацию.

  • Нажмите ОК.

Используем UseExceptionHandler middleware в ASP.NET Core.

Чтобы реализовать глобальный обработчик исключений, мы можем воспользоваться преимуществами встроенного Middleware ASP.NET Core. Middleware представляет из себя программный компонент, внедренный в конвейер обработки запросов, который каким-либо образом обрабатывает запросы и ответы. Мы можем использовать встроенное middleware ASP.NET Core UseExceptionHandler в качестве глобального обработчика исключений. Конвейер обработки запросов ASP.NET Core включает в себя цепочку middleware-компонентов. Эти компоненты, в свою очередь, содержат серию делегатов запросов, которые вызываются один за другим. В то время как входящие запросы проходят через каждый из middleware-компонентов в конвейере, каждый из этих компонентов может либо обработать запрос, либо передать запрос следующему компоненту в конвейере.

С помощью этого middleware мы можем получить всю детализированную информацию об объекте исключения, такую ​​как стектрейс, вложенное исключение, сообщение и т. д., а также вернуть эту информацию через API в качестве вывода. Нам нужно поместить middleware обработки исключений в configure() файла startup.cs. Если мы используем какое-либо приложение на основе MVC, мы можем использовать middleware обработки исключений, как это показано ниже. Этот фрагмент кода демонстрирует, как мы можем настроить middleware UseExceptionHandler для перенаправления пользователя на страницу с ошибкой при возникновении любого типа исключения.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{  
    app.UseExceptionHandler("/Home/Error");  
    app.UseMvc();  
} 

Теперь нам нужно проверить сообщение об исключении. Для этого откройте файл WeatherForecastController.cs и добавьте следующий экшн-метод, чтобы пробросить исключение:

[Route("GetExceptionInfo")]  
[HttpGet]  
public IEnumerable<string> GetExceptionInfo()  
{  
     string[] arrRetValues = null;  
     if (arrRetValues.Length > 0)  
     { }  
     return arrRetValues;  
} 

Если мы хотим получить подробную информацию об объектах исключения, например, стектрейс, сообщение и т. д., мы можем использовать приведенный ниже код в качестве middleware исключения —

app.UseExceptionHandler(  
                options =>  
                {  
                    options.Run(  
                        async context =>  
                        {  
                            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;  
                            context.Response.ContentType = "text/html";  
                            var exceptionObject = context.Features.Get<IExceptionHandlerFeature>();  
                            if (null != exceptionObject)  
                            {  
                                var errorMessage = $"<b>Exception Error: {exceptionObject.Error.Message} </b> {exceptionObject.Error.StackTrace}";  
                                await context.Response.WriteAsync(errorMessage).ConfigureAwait(false);  
                            }  
                        });  
                }  
            );  

Для проверки вывода просто запустите эндпоинт API в любом браузере:

Определение пользовательского Middleware для обработки исключений в API ASP.NET Core

Кроме того, мы можем написать собственное middleware для обработки любых типов исключений. В этом разделе мы продемонстрируем, как создать типичный пользовательский класс middleware. Пользовательское middleware также обеспечивает гораздо большую гибкость для обработки исключений. Мы можем добавить стекатрейс, имя типа исключения, код ошибки или что-нибудь еще, что мы захотим включить как часть сообщения об ошибке. В приведенном ниже фрагменте кода показан типичный пользовательский класс middleware:

using Microsoft.AspNetCore.Http;    
using Newtonsoft.Json;    
using System;    
using System.Collections.Generic;    
using System.Linq;    
using System.Net;    
using System.Threading.Tasks;    
    
namespace API.DemoSample.Exceptions    
{    
    public class ExceptionHandlerMiddleware    
    {    
        private readonly RequestDelegate _next;    
    
        public ExceptionHandlerMiddleware(RequestDelegate next)    
        {    
            _next = next;    
        }    
    
        public async Task Invoke(HttpContext context)    
        {    
            try    
            {    
                await _next.Invoke(context);    
            }    
            catch (Exception ex)    
            {    
                    
            }    
        }    
    }    
} 

В приведенном выше классе делегат запроса передается любому middleware. Middleware либо обрабатывает его, либо передает его следующему middleware в цепочке. Если запрос не успешен, будет выброшено исключение, а затем будет выполнен метод HandleExceptionMessageAsync в блоке catch. Итак, давайте обновим код метода Invoke, как показано ниже:

public async Task Invoke(HttpContext context)  
{  
    try  
    {  
        await _next.Invoke(context);  
    }  
    catch (Exception ex)  
    {  
        await HandleExceptionMessageAsync(context, ex).ConfigureAwait(false);  
    }  
}  

 Теперь нам нужно реализовать метод HandleExceptionMessageAsync, как показано ниже:

private static Task HandleExceptionMessageAsync(HttpContext context, Exception exception)  
{  
    context.Response.ContentType = "application/json";  
    int statusCode = (int)HttpStatusCode.InternalServerError;  
    var result = JsonConvert.SerializeObject(new  
    {  
        StatusCode = statusCode,  
        ErrorMessage = exception.Message  
    });  
    context.Response.ContentType = "application/json";  
    context.Response.StatusCode = statusCode;  
    return context.Response.WriteAsync(result);  
} 

Теперь, на следующем шаге, нам нужно создать статический класс с именем ExceptionHandlerMiddlewareExtensions и добавить приведенный ниже код в этот класс,

using Microsoft.AspNetCore.Builder;  
using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Threading.Tasks;  
  
namespace API.DemoSample.Exceptions  
{  
    public static class ExceptionHandlerMiddlewareExtensions  
    {  
        public static void UseExceptionHandlerMiddleware(this IApplicationBuilder app)  
        {  
            app.UseMiddleware<ExceptionHandlerMiddleware>();  
        }  
    }  
}  

На последнем этапе, нам нужно включить наше пользовательское middleware в методе Configure класса startup, как показано ниже:

app.UseExceptionHandlerMiddleware();  

Заключение

Обработка исключений — это по сути сквозная функциональность для любого типа приложений. В этой статье мы обсудили процесс реализации концепции глобальной обработки исключений. Мы можем воспользоваться преимуществами глобальной обработки исключений в любом приложении ASP.NET Core, чтобы гарантировать, что каждое исключение будет перехвачено и вернет правильные сведения, связанные с этим исключением. С глобальной обработкой исключений нам достаточно в одном месте написать код, связанный с обработкой исключений, для всего нашего приложения. Любые предложения, отзывы или запросы, связанные с этой статьей, приветствуются.


Узнать подробнее о курсе «C# ASP.NET Core разработчик».

Посмотреть вебинар на тему «Отличия структурных шаблонов проектирования на примерах».

Handling errors in an ASP.NET Core Web API

This post looks at the best ways to handle exceptions, validation and other invalid requests such as 404s in ASP.NET Core Web API projects and how these approaches differ from MVC error handling.

Why do we need a different approach from MVC?

In .Net Core, MVC and Web API have been combined so you now have the same controllers for both MVC actions and API actions. However, despite the similarities, when it comes to error handling, you almost certainly want to use a different approach for API errors.

MVC actions are typically executed as a result of a user action in the browser so returning an error page to the browser is the correct approach. With an API, this is not generally the case.

API calls are most often called by back-end code or javascript code and in both cases, you never want to simply display the response from the API. Instead we check the status code and parse the response to determine if our action was successful, displaying data to the user as necessary. An error page is not helpful in these situations. It bloats the response with HTML and makes client code difficult because JSON (or XML) is expected, not HTML.

While we want to return information in a different format for Web API actions, the techniques for handling errors are not so different from MVC. Much of the time, it is practically the same flow but instead of returning a View, we return JSON. Let’s look at a few examples.

The minimal approach

With MVC actions, failure to display a friendly error page is unacceptable in a professional application. With an API, while not ideal, empty response bodies are far more permissible for many invalid request types. Simply returning a 404 status code (with no response body) for an API route that does not exist may provide the client with enough information to fix their code.

With zero configuration, this is what ASP.NET Core gives us out of the box.

Depending on your requirements, this may be acceptable for many common status codes but it will rarely be sufficient for validation failures. If a client passes you invalid data, returning a 400 Bad Request is not going to be helpful enough for the client to diagnose the problem. At a minimum, we need to let them know which fields are incorrect and ideally, we would return an informative message for each failure.

With ASP.NET Web API, this is trivial. Assuming that we are using model binding, we get validation for free by using data annotations and/or IValidatableObject. Returning the validation information to the client as JSON is one easy line of code.

Here is our model:

public class GetProductRequest : IValidatableObject
{
    [Required]
    public string ProductId { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (...)
        {
            yield return new ValidationResult("ProductId is invalid", new[] { "ProductId" });
        }
    }
}

And our controller action:

[HttpGet("product")]
public IActionResult GetProduct(GetProductRequest request)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    ...
}

A missing ProductId results in a 400 status code plus a JSON response body similar to the following:

{
    "ProductId":["The ProductId field is required."]
}

This provides an absolute minimum for a client to consume our service but it is not difficult to improve upon this baseline and create a much better client experience. In the next few sections we will look at how simple it is to take our service to the next level.

Returning additional information for specific errors

If we decide that a status code only approach is too bare-bones, it is easy to provide additional information. This is highly recommended. There are many situations where a status code by itself is not enough to determine the cause of failure. If we take a 404 status code as an example, in isolation, this could mean:

  • We are making the request to the wrong site entirely (perhaps the ‘www’ site rather than the ‘api’ subdomain)
  • The domain is correct but the URL does not match a route
  • The URL correctly maps to a route but the resource does not exist

If we could provide information to distinguish between these cases, it could be very useful for a client. Here is our first attempt at dealing with the last of these:

[HttpGet("product")]
public async Task<IActionResult> GetProduct(GetProductRequest request)
{
    ...

    var model = await _db.Get(...);

    if (model == null)
    {
        return NotFound("Product not found");
    }

    return Ok(model);
}

We are now returning a more useful message but it is far from perfect. The main problem is that by using a string in the NotFound method, the framework will return this string as a plain text response rather than JSON.

As a client, a service returning a different content type for certain errors is much harder to deal with than a consistent JSON service.

This issue can quickly be rectified by changing the code to what is shown below but in the next section, we will talk about a better alternative.

return NotFound(new { message = "Product not found" });

Customising the response structure for consistency

Constructing anonymous objects on the fly is not the approach to take if you want a consistent client experience. Ideally our API should return the same response structure in all cases, even when the request was unsuccessful.

Let’s define a base ApiResponse class:

public class ApiResponse
{
    public int StatusCode { get; }

    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string Message { get; }

    public ApiResponse(int statusCode, string message = null)
    {
        StatusCode = statusCode;
        Message = message ?? GetDefaultMessageForStatusCode(statusCode);
    }

    private static string GetDefaultMessageForStatusCode(int statusCode)
    {
        switch (statusCode)
        {
            ...
            case 404:
                return "Resource not found";
            case 500:
                return "An unhandled error occurred";
            default:
                return null;
        }
    }
}

We’ll also need a derived ApiOkResponse class that allows us to return data:

public class ApiOkResponse : ApiResponse
{
    public object Result { get; }

    public ApiOkResponse(object result)
        :base(200)
    {
        Result = result;
    }
}

Finally, let’s declare an ApiBadRequestResponse class to handle validation errors (if we want our responses to be consistent, we will need to replace the built-in functionality used above).

public class ApiBadRequestResponse : ApiResponse
{
    public IEnumerable<string> Errors { get; }

    public ApiBadRequestResponse(ModelStateDictionary modelState)
        : base(400)
    {
        if (modelState.IsValid)
        {
            throw new ArgumentException("ModelState must be invalid", nameof(modelState));
        }

        Errors = modelState.SelectMany(x => x.Value.Errors)
            .Select(x => x.ErrorMessage).ToArray();
    }
}

These classes are very simple but can be customised to your own requirements.

If we change our action to use these ApiResponse based classes, it becomes:

[HttpGet("product")]
public async Task<IActionResult> GetProduct(GetProductRequest request)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(new ApiBadRequestResponse(ModelState));
    }

    var model = await _db.Get(...);

    if (model == null)
    {
        return NotFound(new ApiResponse(404, $"Product not found with id {request.ProductId}"));
    }

    return Ok(new ApiOkResponse(model));
}

The code is slightly more complicated now but all three types of response from our action (success, bad request and not found) now use the same general structure.

Centralising Validation Logic

Given that validation is something that you do in practically every action, it makes to refactor this generic code into an action filter. This reduces the size of our actions, removes duplicated code and improves consistency.

public class ApiValidationFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new BadRequestObjectResult(new ApiBadRequestResponse(context.ModelState));
        }

        base.OnActionExecuting(context);
    }
}

Handling global errors

Responding to bad input in our controller actions is the best way to provide specific error information to our client. Sometimes however, we need to respond to more generic issues. Examples of this include:

  • A 401 Unauthorized code returned from security middleware.

  • A request URL that does not map to a controller action resulting in a 404.

  • Global exceptions. Unless you can do something about a specific exception, you should not clutter your actions with try catch blocks.

As with MVC, the easiest way to deal with global errors is by using StatusCodePagesWithReExecute and UseExceptionHandler.

We talked about StatusCodePagesWithReExecute last time but to reiterate, when a non-success status code is returned from inner middleware (such as an API action), the middleware allows you to execute another action to deal with the status code and return a custom response.

UseExceptionHandler works in a similar way, catching and logging unhandled exceptions and allowing you to execute another action to handle the error. In this example, we configure both pieces of middleware to point to the same action.

We add the middleware in startup.cs:

app.UseStatusCodePagesWithReExecute("/error/{0}");
app.UseExceptionHandler("/error/500");
...
//register other middleware that might return a non-success status code

Then we add our error handling action:

[Route("error/{code}")]
public IActionResult Error(int code)
{
    return new ObjectResult(new ApiResponse(code));
}

With this in place, all exceptions and non-success status codes (without a response body) will be handled by our error action where we return our standard ApiResponse.

Custom Middleware

For the ultimate in control, you can replace or complement built-in middleware with your own custom middleware. The example below handles any bodiless response and returns our simple ApiResponse object as JSON. If this is used in conjunction with code in our actions to return ApiResponse objects, we can ensure that both success and failure responses share the same common structure and all requests result in both a status code and a consistent JSON body:

public class ErrorWrappingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ErrorWrappingMiddleware> _logger;
    
    public ErrorWrappingMiddleware(RequestDelegate next, ILogger<ErrorWrappingMiddleware> logger)
    {
        _next = next;
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next.Invoke(context);
        }
        catch(Exception ex)
        {
            _logger.LogError(EventIds.GlobalException, ex, ex.Message);

            context.Response.StatusCode = 500;
        }            

        if (!context.Response.HasStarted)
        {
            context.Response.ContentType = "application/json";

            var response = new ApiResponse(context.Response.StatusCode);

            var json = JsonConvert.SerializeObject(response);

            await context.Response.WriteAsync(json);
        }            
    }
}

Conclusion

Handling errors in ASP.NET Core APIs is similar but different from MVC error code. At the action level, we want to return custom objects (serialised as JSON) rather than custom views.

For generic errors, we can still use the StatusCodePagesWithReExecute middleware but need to modify our code to return an ObjectResult instead of a ViewResult.

For full control, it is not difficult to write your own middleware to handle errors exactly as required.

Useful or Interesting?

If you liked the article, I would really appreciate it if you could share it with your Twitter followers.

Share
on Twitter

Comments

Quick and Easy Exception Handling

Simply add this middleware before ASP.NET routing into your middleware registrations.

app.UseExceptionHandler(c => c.Run(async context =>
{
    var exception = context.Features
        .Get<IExceptionHandlerPathFeature>()
        .Error;
    var response = new { error = exception.Message };
    await context.Response.WriteAsJsonAsync(response);
}));
app.UseMvc(); // or .UseRouting() or .UseEndpoints()

Done!


Enable Dependency Injection for logging and other purposes

Step 1. In your startup, register your exception handling route:

// It should be one of your very first registrations
app.UseExceptionHandler("/error"); // Add this
app.UseEndpoints(endpoints => endpoints.MapControllers());

Step 2. Create controller that will handle all exceptions and produce error response:

[AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)]
public class ErrorsController : ControllerBase
{
    [Route("error")]
    public MyErrorResponse Error()
    {
        var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
        var exception = context.Error; // Your exception
        var code = 500; // Internal Server Error by default

        if      (exception is MyNotFoundException) code = 404; // Not Found
        else if (exception is MyUnauthException)   code = 401; // Unauthorized
        else if (exception is MyException)         code = 400; // Bad Request

        Response.StatusCode = code; // You can use HttpStatusCode enum instead

        return new MyErrorResponse(exception); // Your error model
    }
}

A few important notes and observations:

  • You can inject your dependencies into the Controller’s constructor.
  • [ApiExplorerSettings(IgnoreApi = true)] is needed. Otherwise, it may break your Swashbuckle swagger
  • Again, app.UseExceptionHandler("/error"); has to be one of the very top registrations in your Startup Configure(...) method. It’s probably safe to place it at the top of the method.
  • The path in app.UseExceptionHandler("/error") and in controller [Route("error")] should be the same, to allow the controller handle exceptions redirected from exception handler middleware.

Here is the link to official Microsoft documentation.


Response model ideas.

Implement your own response model and exceptions.
This example is just a good starting point. Every service would need to handle exceptions in its own way. With the described approach you have full flexibility and control over handling exceptions and returning the right response from your service.

An example of error response model (just to give you some ideas):

public class MyErrorResponse
{
    public string Type { get; set; }
    public string Message { get; set; }
    public string StackTrace { get; set; }

    public MyErrorResponse(Exception ex)
    {
        Type = ex.GetType().Name;
        Message = ex.Message;
        StackTrace = ex.ToString();
    }
}

For simpler services, you might want to implement http status code exception that would look like this:

public class HttpStatusException : Exception
{
    public HttpStatusCode Status { get; private set; }

    public HttpStatusException(HttpStatusCode status, string msg) : base(msg)
    {
        Status = status;
    }
}

This can be thrown from anywhere this way:

throw new HttpStatusCodeException(HttpStatusCode.NotFound, "User not found");

Then your handling code could be simplified to just this:

if (exception is HttpStatusException httpException)
{
    code = (int) httpException.Status;
}

HttpContext.Features.Get<IExceptionHandlerFeature>() WAT?

ASP.NET Core developers embraced the concept of middlewares where different aspects of functionality such as Auth, MVC, Swagger etc. are separated and executed sequentially in the request processing pipeline. Each middleware has access to request context and can write into the response if needed. Taking exception handling out of MVC makes sense if it’s important to handle errors from non-MVC middlewares the same way as MVC exceptions, which I find is very common in real world apps. So because built-in exception handling middleware is not a part of MVC, MVC itself knows nothing about it and vice versa, exception handling middleware doesn’t really know where the exception is coming from, besides of course it knows that it happened somewhere down the pipe of request execution. But both may needed to be «connected» with one another. So when exception is not caught anywhere, exception handling middleware catches it and re-runs the pipeline for a route, registered in it. This is how you can «pass» exception handling back to MVC with consistent content negotiation or some other middleware if you wish. The exception itself is extracted from the common middleware context. Looks funny but gets the job done :).

| Воскресенье, 3 марта, 2013

Метки: ASP.NET Web API Комментарии: 0

ASP.NET Web API упрощает разработку HTTP-сервисов, а также предоставляет много способов возврата полных и информативных сообщений об ошибках для различных ситуаций. Рассмотрим эти возможности.

Для начала, посмотрим как выглядит обычное сообщение об ошибке для Web API:

{ 
"Message": "No HTTP resource was found that matches the request URI 'http://localhost/Foo'.", 
"MessageDetail": "No type was found that matches the controller named 'Foo'." 
}

То есть ошибка, это просто коллекция пар ключей и значений, которая сообщает нам, что пошло не так. Эта коллекция отсылается клиенту через HTTP-запросы. В примере выше содержимое представлено в формате JSON.

Но если указать в запросе в заголовке Accept «text/html», то ответ будет в xml виде:

<Error> 
  <Message>No HTTP resource was found that matches the request URI 'http://localhost/Foo'.</Message> 
  <MessageDetail>No type was found that matches the controller named 'Foo'.</MessageDetail> 
</Error>

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

HttpError

Примеры приведенные выше — это объекты HttpError, сериализованные с помощью Json.NET и DataContactSerializer.

Тип HttpError определяет содержательный и расширяемый способ передачи информации об ошибках между различными платформами. Фактически, это просто класс Dictionary, который предоставляет еще несколько вспомогательных конструкторов для создания сообщений об ошибках, исключениях и некорректных состояниях модели.

public HttpError();
public HttpError(string message);
public HttpError(Exception exception, bool includeErrorDetail);
public HttpError(ModelStateDictionary modelState, bool includeErrorDetail);

Так выглядят исключения в формате JSON:

{ 
"Message": "An error has occurred.", 
"ExceptionMessage": "Index was outside the bounds of the array.", 
"ExceptionType": "System.IndexOutOfRangeException", 
"StackTrace": "   at WebApiTest.TestController.Post(Uri uri) in c:\Temp\WebApiTest\WebApiTest\TestController.cs:line 18rn   at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClassf.<GetExecutor>b__9(Object instance, Object[] methodParameters)rn   at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Object instance, Object[] arguments)rn   at System.Threading.Tasks.TaskHelpers.RunSynchronously[TResult](Func`1 func, CancellationToken cancellationToken)" 
}

Так некорректная модель:

{ 
  "Message": "The request is invalid.", 
  "ModelState": 
   { 
       "s": [ "Required property 's' not found in JSON. Path '', line 1, position 2."  ] 
   }
} 

Можно создавать и собственные виды ошибок в классе HttpError, например так:

public HttpResponseMessage Get()
{
   HttpError myCustomError = new HttpError("My custom error message") { { "CustomErrorCode", 37 } };
   return Request.CreateErrorResponse(HttpStatusCode.BadRequest, myCustomError);
}

Результат будет таким:

{ 
  "Message": "My custom error message", 
  "CustomErrorCode": 37 
}

В этом примере метод возвращает ошибку с помощью Request.CreateErrorMessage. Пользовательская ошибка обернута в класс HttpResponseMessage. Это рекомендованный способ создания ответов c сообщениями об ошибках. Ниже приведены расширяемые методы для возврата такой информации:

public static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, Exception exception);
public static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, HttpError error);
public static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, ModelStateDictionary modelState);
public static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, string message);
public static HttpResponseMessage CreateErrorResponse(this HttpRequestMessage request, HttpStatusCode statusCode, string message, Exception exception);

Но можно и просто генерировать любое исключение в методе действия. Web API автоматически перехватывает исключение, конвертирует в ответ сервера с кодом 500 (Internal Server Error) и возвращает сообщение в точно таком же формате, как и с методом CreateErrorResponse.

HttpResponseException

Но как быть, если нужно вернуть ошибку в методе, который возвращает объекты не являющиеся классами HttpResponseMessage? Тут на помощь приходит класс HttpResponseException. Данный тип исключения определен специально для Web API и служит двум целям:

  1. Позволяет возвращать специальные ответы HTTP из методов, которые не возвращают HttpResponseMethod.
  2. Упрощает код в методах Web API, выполняя вывод ответа сервера с информацией об ошибке немедленно.

Технически, HttpResponseException может быть использован для возврата любых http-ответов, но особенно полезен данный класс для ошибок. Это позволяет нам писать методы следующим образом:

public Person Get(int id)
{
   if (!_contacts.ContainsKey(id))
   {
      throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.NotFound, String.Format("Contact {0} not found.", id)));
   }
   return _contacts[id];
}

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

Подробное описание ошибок

В начале статьи в примере упоминался параметр «includeErrorDetails» в конструкторе класса HttpError. Это для случаев, когда клиенту нужно предоставить отладочную информацию, для разрешения проблем на сервере. По умолчанию подробная информация не высылается удаленным клиентам, но предоставляется клиентам на локальном компьютере. В самом первом примере статьи, локальный клиент получит:

{ 
"Message": "No HTTP resource was found that matches the request URI 'http://localhost/Foo'.", 
"MessageDetail": "No type was found that matches the controller named 'Foo'." 
}

А до удаленного клиента дойдет только:

{ 
"Message": "No HTTP resource was found that matches the request URI 'http://localhost/Foo'." 
}

MessageDetails содержит специфичную информацию, которую удаленные клиенты в большинстве случаев видеть не должны. То есть ошибки содержащие отладочную информацию (exception message, exception type, stack trace) не предоставляют подробное описание по умолчанию, но ошибки состояния моделей (кроме исключений) подробно отсылаются и удаленным клиентам.

Явно задавать возврат подробной информации можно в объекте HttpConfiguration:

config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.LocalOnly;
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Never;

Как было упомянуто выше, значение по умолчанию LocalOnly.

Обработка ошибок для некорректных моделей

Еще один распространенный способ использования обработки ошибок Web API – это немедленный возврат ошибки в случае некорректной модели. Для этого лучше всего реализовать следующий фильтр метода действия:

public class ValidationFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.ModelState.IsValid)
        {
            actionContext.Response = 
                 actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest,   
                 actionContext.ModelState);
         }
     }
}

Можно прописать этот фильтр для каждого метода а можно зарегистрировать его глобально сразу для всех методов:

config.Filters.Add(new ValidationFilterAttribute());

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

Никто еще не оставил здесь комментарий.

The exception handling features help us deal with the unforeseen errors which could appear in our code.  To handle exceptions we can use the try-catch block in our code as well as finally keyword to clean up resources afterward.

Even though there is nothing wrong with the try-catch blocks in our Actions in Web API project, we can extract all the exception handling logic into a single centralized place. By doing that, we make our actions more readable and the error handling process more maintainable. If we want to make our actions even more readable and maintainable, we can implement Action Filters. We won’t talk about action filters in this article but we strongly recommend reading our post Action Filters in .NET Core.

In this article, we are going to handle errors by using a try-catch block first and then rewrite our code by using built-in middleware and our custom middleware for global error handling to demonstrate the benefits of this approach. We are going to use an ASP.NET Core Web API project to explain these features and if you want to learn more about it (which we strongly recommend), you can read our ASP.NET Core Web API Tutorial.


VIDEO: Global Error Handling in ASP.NET Core Web API video.


To download the source code for our starting project, you can visit the Global error handling start project.

For the finished project refer to Global error handling end project.

Let’s start.

Error Handling With Try-Catch Block

To start off with this example, let’s open the Values Controller from the starting project (Global-Error-Handling-Start project). In this project, we can find a single Get() method and an injected Logger service.

It is a common practice to include the log messages while handling errors, therefore we have created the LoggerManager service. It logs all the messages to the C drive, but you can change that by modifying the path in the nlog.config file. For more information about how to use Nlog in .NET Core, you can visit Logging with NLog.

Now, let’s modify our action method to return a result and log some messages:

using System;
using LoggerService;
using Microsoft.AspNetCore.Mvc;

namespace GlobalErrorHandling.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private ILoggerManager _logger;

        public ValuesController(ILoggerManager logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IActionResult Get()
        {
            try
            {
                _logger.LogInfo("Fetching all the Students from the storage");

                var students = DataManager.GetAllStudents(); //simulation for the data base access

                _logger.LogInfo($"Returning {students.Count} students.");

                return Ok(students);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Something went wrong: {ex}");
                return StatusCode(500, "Internal server error");
            }
        }
    }
}

When we send a request at this endpoint, we will get this result:

Basic request - Global Error Handling

And the log messages:

log basic request - Global Error Handling

We see that everything is working as expected.

Now let’s modify our code, right below the GetAllStudents() method call, to force an exception:

throw new Exception("Exception while fetching all the students from the storage.");

Now, if we send a request:

try catche error - Global Error Handling

And the log messages:

log try catch error

So, this works just fine. But the downside of this approach is that we need to repeat our try-catch blocks in all the actions in which we want to catch unhandled exceptions. Well, there is a better approach to do that.

Handling Errors Globally With the Built-In Middleware

The UseExceptionHandler middleware is a built-in middleware that we can use to handle exceptions in our ASP.NET Core Web API application. So, let’s dive into the code to see this middleware in action.

First, we are going to add a new class ErrorDetails in the Models folder:

using System.Text.Json;

namespace GlobalErrorHandling.Models
{
    public class ErrorDetails
    {
        public int StatusCode { get; set; }
        public string Message { get; set; }

        public override string ToString()
        {
            return JsonSerializer.Serialize(this);
        }
    }
}

We are going to use this class for the details of our error message.

To continue, let’s create a new folder Extensions and a new static class ExceptionMiddlewareExtensions.cs inside it.

Now, we need to modify it:

using GlobalErrorHandling.Models;
using LoggerService;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using System.Net;

namespace GlobalErrorHandling.Extensions
{
    public static class ExceptionMiddlewareExtensions
    {
        public static void ConfigureExceptionHandler(this IApplicationBuilder app, ILoggerManager logger)
        {
            app.UseExceptionHandler(appError =>
            {
                appError.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($"Something went wrong: {contextFeature.Error}");

                        await context.Response.WriteAsync(new ErrorDetails()
                        {
                            StatusCode = context.Response.StatusCode,
                            Message = "Internal Server Error."
                        }.ToString());
                    }
                });
            });
        }
    }
}

In the code above, we’ve created an extension method in which we’ve registered the UseExceptionHandler middleware. Then, we populate the status code and the content type of our response, log the error message and finally return the response with the custom-created object.

To be able to use this extension method, let’s modify the Configure method inside the Startup class for .NET 5 project:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerManager logger) 
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.ConfigureExceptionHandler(logger);

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

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

Or if you are using .NET 6 and above:

var app = builder.Build();

var logger = app.Services.GetRequiredService<ILoggerManager>();
app.ConfigureExceptionHandler(logger);

Finally, let’s remove the try-catch block from our code:

public IActionResult Get()
{
    _logger.LogInfo("Fetching all the Students from the storage");

     var students = DataManager.GetAllStudents(); //simulation for the data base access

     throw new Exception("Exception while fetching all the students from the storage.");

     _logger.LogInfo($"Returning {students.Count} students.");

     return Ok(students);
}

And there you go. Our action method is much cleaner now and what’s more important we can reuse this functionality to write more readable actions in the future.

So let’s inspect the result:

Global Handler Middleware

And the log messages:

log global handler middleware

Excellent.

Now, we are going to use custom middleware for global error handling.

Handling Errors Globally With the Custom Middleware

Let’s create a new folder named CustomExceptionMiddleware and a class ExceptionMiddleware.cs inside it.

We are going to modify that class:

public class ExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILoggerManager _logger;

    public ExceptionMiddleware(RequestDelegate next, ILoggerManager logger)
    {
        _logger = logger;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception ex)
        {
            _logger.LogError($"Something went wrong: {ex}");
            await HandleExceptionAsync(httpContext, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        await context.Response.WriteAsync(new ErrorDetails()
        {
            StatusCode = context.Response.StatusCode,
            Message = "Internal Server Error from the custom middleware."
        }.ToString());
    }
}

The first thing we need to do is to register our IloggerManager service and RequestDelegate through the dependency injection. The _next parameter of RequestDeleagate type is a function delegate that can process our HTTP requests.

After the registration process, we create the InvokeAsync() method. RequestDelegate can’t process requests without it.

If everything goes well, the _next delegate should process the request and the Get action from our controller should generate a successful response. But if a request is unsuccessful (and it is, because we are forcing an exception), our middleware will trigger the catch block and call the HandleExceptionAsync method.

In that method, we just set up the response status code and content type and return a response.

Now let’s modify our ExceptionMiddlewareExtensions class with another static method:

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

In .NET 6 and above, we have to extend the WebApplication type:

public static void ConfigureCustomExceptionMiddleware(this WebApplication app) 
{ 
    app.UseMiddleware<ExceptionMiddleware>(); 
}

Finally, let’s use this method in the Configure method in the Startup class:

//app.ConfigureExceptionHandler(logger);
app.ConfigureCustomExceptionMiddleware();

Great.

Now let’s inspect the result again:

custom handler middleware

There we go. Our custom middleware is implemented in a couple of steps.

Customizing Error Messages

If you want, you can always customize your error messages from the error handler. There are different ways of doing that, but we are going to show you the basic two ways.

First of all, we can assume that the AccessViolationException is thrown from our action:

[HttpGet]
public IActionResult Get()
{
    _logger.LogInfo("Fetching all the Students from the storage");

    var students = DataManager.GetAllStudents(); //simulation for the data base access

    throw new AccessViolationException("Violation Exception while accessing the resource.");

    _logger.LogInfo($"Returning {students.Count} students.");

    return Ok(students);
}

Now, what we can do is modify the InvokeAsync method inside the ExceptionMiddleware.cs class by adding a specific exception checking in the additional catch block:

public async Task InvokeAsync(HttpContext httpContext)
{
    try
    {
        await _next(httpContext);
    }
    catch (AccessViolationException avEx)
    {
        _logger.LogError($"A new violation exception has been thrown: {avEx}");
        await HandleExceptionAsync(httpContext, avEx);
    }
    catch (Exception ex)
    {
        _logger.LogError($"Something went wrong: {ex}");
        await HandleExceptionAsync(httpContext, ex);
    }
}

And that’s all. Now if we send another request with Postman, we are going to see in the log file that the AccessViolationException message is logged. Of course, our specific exception check must be placed before the global catch block.

With this solution, we are logging specific messages for the specific exceptions, and that can help us, as developers, a lot when we publish our application. But if we want to send a different message for a specific error, we can also modify the HandleExceptionAsync method in the same class:

private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
    context.Response.ContentType = "application/json";
    context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

    var message = exception switch
    {
        AccessViolationException =>  "Access violation error from the custom middleware",
        _ => "Internal Server Error from the custom middleware."
    };

    await context.Response.WriteAsync(new ErrorDetails()
    {
        StatusCode = context.Response.StatusCode,
        Message = message
    }.ToString());
}

Here, we are using a switch expression pattern matching to check the type of our exception and assign the right message to the message variable. Then, we just use that variable in the WriteAsync method.

Now if we test this, we will get a log message with the Access violation message, and our response will have a new message as well:

{
    "StatusCode": 500,
    "Message": "Access violation error from the custom middleware"
}

One thing to mention here. We are using the 500 status code for all the responses from the exception middleware, and that is something we believe it should be done. After all, we are handling exceptions and these exceptions should be marked with a 500 status code. But this doesn’t have to be the case all the time. For example, if you have a service layer and you want to propagate responses from the service methods as custom exceptions and catch them inside the global exception handler, you may want to choose a more appropriate status code for the response. You can read more about this technique in our Onion Architecture article. It really depends on your project organization.

Conclusion

That was awesome.

We have learned, how to handle errors in a more sophisticated way and cleaner as well. The code is much more readable and our exception handling logic is now reusable for the entire project.

Thank you for reading this article. We hope you have learned new useful things.

Понравилась статья? Поделить с друзьями:
  • We not going to school today найдите ошибки
  • Wifire ошибка 301
  • We looking for a new flat исправить ошибки
  • Wifire ошибка 2800 ошибка подключения к источнику
  • We lived here since 1997 исправить ошибки