Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Mar 12, 2021

.NET 5.0 Web Api global error handling

Автор:
Bruno Joaquim
Источник:
Просмотров:
3152

Exceptions are part of any software and can happen any time even where we are absolutely sure that nothing can go wrong. Exceptions are inevitable, we all agree with that. We should always deal with them in our applications to avoid any kind of unrecoverable crash.

In order to avoid any chaotic scenario that could happen, we should handle our exceptions in a way that could help us deal with the particular problem, by logging that error, creating metrics, and other kinds of error monitoring.

In API applications, is very common we raise an exception in the process of validating the request due to some bad data provided by the client. In cases like this, it’s interesting for the API to respond with a standard and clear error response, making it easy for the clients to understand and act according to solve the problem. That’s what we going to cover today in this post.

Creating the playground project

First let’s briefly go through the process of creating the playground project, which we will use as an example to implement our Global Error handling. I will use VS 2019 to create the project, but the process should be similar on newest versions of VS. Let’s begin by selecting an ASP.NET Core Web Application project:

Next, let’s select ASP.NET Core Web API and hit the create button. VS will create the API which will have only one single controller and will be ready for use. That’s more than necessary for us to start working on our example.

Not handling exceptions

Let’s begin seeing what happens when a request arrives on the controller layer and an exception is raised without any handling. In the controller WeatherForecastController let’s purposefully throw an exception:

[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
    throw new Exception("Ops..., An error occurred processing the request");
}

Using Postman to make the request to this endpoint, we should see a response like this:

Here we are responding to the client the whole stack of the error. The exception message itself can be useful, but for the client, it’s not particularly necessary to know the whole stack of the error.

The worst part is that we are not even serializing the error in a standard way, like JSON for example. Let’s improve this by handling the error and serializing it in JSON.

public class GlobalErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;

    public GlobalErrorHandlingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next.Invoke(context);
        }
        catch (Exception ex)
        {
            var response = context.Response;
            response.ContentType = "application/json";
            response.StatusCode = (int)HttpStatusCode.InternalServerError;

            var errorResponse = new
            {
                message = ex.Message,
                statusCode = response.StatusCode
            };

            var errorJson = JsonSerializer.Serialize(errorResponse);

            await response.WriteAsync(errorJson);
        }
    }
}

Now we are using try/catch to handle the error. When something wrong happens, an error response object with message and statusCode is created and serialized on the response stream. Let’s make the same request to see what changed from the last response:

Now we see some improvement. The error now it’s very clear and standardized to the JSON format. Our clients can now map this error very easily, and we can add more fields to this response as our application grows.

As the application grows, do you agree that more controllers/methods will be created? It’s interesting for us, productive programmers, to duplicate this try/ catch logic to every endpoint of our application? Probably no. What we can do to centralize this error handling? Let’s create a middleware to handle this for us.

Global Error Handling Middleware

First of all, what is middleware? In a simple way, Middleware is some code that is executed in every request that’s processed by the API Application. The middleware can act on the request before the controller handles it and on the response after the controller logic is executed.


Middlewares pipeline

That’s exactly what we need, if anything wrong happens when the controller is handling the request, we should be able to handle the exception and return a meaningful response. Let’s create the middleware and move the error handling logic for it:

public class GlobalErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;

    public GlobalErrorHandlingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next.Invoke(context);
        }
        catch (Exception ex)
        {
            var response = context.Response;
            response.ContentType = "application/json";
            response.StatusCode = (int)HttpStatusCode.InternalServerError;

            var errorResponse = new
            {
                message = ex.Message,
                statusCode = response.StatusCode
            };

            var errorJson = JsonSerializer.Serialize(errorResponse);

            await response.WriteAsync(errorJson);
        }
    }
}

The next delegate being received by the Middleware constructor parameter indicates the next middleware in the pipeline. In our case, since we have only one middleware, when we call the invoke method we are invoking the controller. Note that the handling logic is exactly the same that was implemented in our controller.

The middleware itself is ready, the next step is to tell the application to use this middleware. Open the Startup.cs file and on the Configure method add this line of code:

app.UseMiddleware<GlobalErrorHandlingMiddleware>();

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

    app.UseRouting();

    app.UseAuthorization();

    app.UseMiddleware<GlobalErrorHandlingMiddleware>();

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

And that’s it, no need to deal with errors in the controller, we can now remove all the error handling logic from it. Every new Endpoint or Controller that we add to this API will be covered by this Middleware, cool right?

Distinguishing between client and server errors

With the error handling logic centralized, we can start implementing some other important features like distinguishing between client and server errors. Client errors (400) are basically bad input and server errors (500) are any kind of bug in our application, like a NullReferenceException.

In order to make this distinction, we should be able to identify, in our middleware, if the exception raised was originally throw in a process of validation or if it was from something that went wrong processing the request.

Let’s create a new class, called BusinessException. We will throw this exception whenever we identify any kind of bad input from the client or any behavior that doesn’t match the business logic.

public class BusinessException : Exception
{
    public BusinessException(string message) : base(message)
    {

    }
}

Now we can make a little adjustment on the middleware to identify when the exception raised is of the type BussinessException. When true, the status code that will be returned on the response will be different than when it was an error raised by some bad implementation or a bug:

public async Task Invoke(HttpContext context)
{
    try
    {
        await _next.Invoke(context);
    }
    catch (Exception ex)
    {
        var response = context.Response;
        response.ContentType = "application/json";

        switch (ex)
        {
            case BusinessException:
                response.StatusCode = (int)HttpStatusCode.BadRequest;
                break;
            default:
                response.StatusCode = (int)HttpStatusCode.InternalServerError;
                break;
        }

        var errorResponse = new
        {
            message = ex.Message,
            statusCode = response.StatusCode
        };

        var errorJson = JsonSerializer.Serialize(errorResponse);

        await response.WriteAsync(errorJson);
    }
}

Just out of curiosity, let’s now change the kind of the exception we are raising on the get method and make the request again using the postman to see what kind of status code we are getting now:

And that’s it, we can update this middleware to map the other several kinds of errors that can happen when processing an API request. I particularly love how middlewares work since we just implement once and reuse its logic through every request that our API will handle. We can use this idea to implement other features, like logging every request, authentication, masking sensitive info, etc.

Take care and happy coding!

The source code is available on GitHub.

Похожее
Feb 26, 2023
Author: Ian Segers
Error handling with Async/Await in JS This will be a small article, based on some issues I picked up during code reviews and discussions with other developers. This article is rather focused on the novice JS developers. A Simple Try...
Aug 26, 2021
Author: Dumindu De Silva
What is .Net 5 .Net 5 is one of the major and latest releases of Microsoft’s .NET family. It comes with many exciting features compared to the previous released .Net core. The primary goal with the release of .Net 5...
Jul 25
Author: Ankit Sahu
Introduction In this article, we are going to discuss How to implement Authentication and Authorization in .NET 8 Web API. This is a continuation of.Net 8 series, so if you are new, Please have a look at my previous articles....
Aug 23, 2022
Author: Luis Rodrigues
Suppose we are building a web api that contains a route to send notification messages to other systems. For security matters, before sending any notification message, we need to provide some credentials to these systems to they accept our messages....
Написать сообщение
Тип
Почта
Имя
*Сообщение