Centralized exception handling and request validation in ASP.NET Core

Β· 3277 words Β· 16 minutes to read

One of the most common things that I have seen developers working with ASP.NET Core struggle with, is the way to centrally and consistently handle application errors and input validation. Those seemingly different topics are really two sides of the same coin.

More often than not, exceptions are just allowed to bubble all the way up and left unhandled, leaving the framework the responsibility to convert them to a generic 500 errors. In many other situations, exception handling is fragmented and happens in certain individual controllers only. With regard to input validation, we often have completely customized ways of notifying the client about input issues or - at best - we leave everything to the framework and let it work its defaults via the ModelState functionality.

What I wanted to show you today is how you can introduce a consistent, centralized way of handling exceptions and request validation in an ASP.NET Core web application.

Problem Details πŸ”—

One of the key things about building usable HTTP APIs is consistency. Having consistent responses in similar situations is absolutely crucial when building a maintainable, usable and predictable API.

As usually in life, there is no need to re-invent the wheel here. RFC7808 actually defines a problem detail type “as a way to carry machine-readable details of errors in a HTTP response to avoid the need to define new error response formats for HTTP APIs”, so it would be a good idea to just embrace that. And that applies both to input validation (4xx responses) and server errors (5xx responses).

What’s even better, is that the ASP.NET Core MVC framework, since version 2.1, actually ships with a built-in POCO for that RFC, called (surprise) ProblemDetails. The comments in the code explain nicely what each property is intended to do, so that you don’t have to read the RFC anymore πŸ˜ƒ

/// <summary> /// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807.

  
/// </summary> 

public class ProblemDetails  
{  
/// <summary> /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when

  
/// dereferenced, it provide human-readable documentation for the problem type  
/// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be  
/// "about:blank".  
/// </summary> 

public string Type { get; set; }

/// <summary> /// A short, human-readable summary of the problem type.It SHOULD NOT change from occurrence to occurrence

  
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation;  
/// see[RFC7231], Section 3.4).  
/// </summary> 

public string Title { get; set; }

/// <summary> /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.

  
/// </summary> 

public int? Status { get; set; }

/// <summary> /// A human-readable explanation specific to this occurrence of the problem.

  
/// </summary> 

public string Detail { get; set; }

/// <summary> /// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced.

  
/// </summary> 

public string Instance { get; set; }  
}

In short, we can think about problem details in the following manner:

  • What has happened? (Title)
  • Where can I learn more about that? (Type - a URI)
  • Why has it happened? (Detail)
  • Where can I learn more about this particular occurrence (Instance - a URI)
  • And the status code is rather obvious

The RFC is pretty clear that the Type should be a general purpose URI that links to an HTML site describing the problem. This is generally not too useful for the purpose of HTTP APIs, as they would often be invoked without a watchful human agent overseeing the process, therefore it’s not unreasonable to skip this in APIs. An alternative is to simply direct there to a general error description section you might have i.e. in a Swagger UI page. That said, the spec mentions that: “when this member is not present, its value is assumed to be about:blank.” Therefore for the purpose of this article we will just skip it completely, and configure our JSON serializer to omit the property altogether.

Instance is also supposed to be URI but the spec is less relaxed about it, as it doesn’t force us to point to an HTML page. You may of course use it to link to some page where individual error details (for this particular occurrence) could be obtained. But, for example, since it is a URI (not URL) you can also interpret it as simply a way for you to convey an occurrence ID (a URN), rather than a physical link that can be visited. This would allow the caller to easily correlate an error received on the client side with the logs, or allows the end user to use that error ID when dealing with customer support. More about that later.

In a case of dealing with exceptions in an API, we could use the Title property to pass some general text, for example “An unexpected error occurred!”. It shouldn’t change from occurrence to occurrence so it’s a good bet. Similarly for input validation, we could say something like “Input validation failed”. In the Detail we will provide extra information, but we will handle this differently depending on whether we are dealing with an exception or input validation failure.

Exception handling πŸ”—

Let’s start with exception handling. ASP.NET Core exposes a feature called IExceptionHandlerFeature which you can use to globally handle exceptions. To wire in such a global exception handler you need to call UseExceptionHandler() method on the IApplicationBuilder:

// global exception handler  
app.UseExceptionHandler(errorApp =>  
{  
// handle  
}

// configure rest of the pipeline  
app.UseMvc();

UseExceptionHandler() allows you to register a de-facto sub-application to handle your application’s exceptions - the delegate you will be registering gives you access to a child IApplicationBuilder. This is hardly ever needed though, and the typical usage scenario would be to simply register a single, lone middleware component there, that handles the exception and returns a relevant response to the caller.

This is shown in the next snippet.

app.UseExceptionHandler(errorApp =>  
{  
errorApp.Run(async context =>  
{  
var errorFeature = context.Features.Get<IExceptionHandlerFeature>();  
var exception = errorFeature.Error;

// log the exception etc..  
// produce some response for the caller  
});  
});

What’s next, is deciding how we actually would like to format the response to the caller in a standardized way. I have seen many HTTP APIs invent their own ways of doing that - which may not be desirable, but if it is at least consistent in the scope of a given system, it’s not that bad.

However, since this is a blog post about the Problem Detail RFC, we’ll be incorporating that. Most of the stuff has already been laid out in the previous section, so it should be clear, but specifically for the Detail part of the response we will do two things:

  • if the caller is trusted (say, coming from localhost, is a developer or an authenticated user who has been allowed to see exception details), we may want to return the full Exception
  • otherwise another generic text would suffice - for example “The instance value should be used to identify the problem when calling customer support”.

Of course it is entirely up to you how you consider to structure your errors, interpret the given RFC properties, refine them to make sense for your applications and what would you like to convey in them - this article is merely an illustration of the technique and how you might want to get started.

Anyway, at code level it all could look as follows:

app.UseExceptionHandler(errorApp =>  
{  
errorApp.Run(async context =>  
{  
var errorFeature = context.Features.Get<IExceptionHandlerFeature>();  
var exception = errorFeature.Error;

// the IsTrusted() extension method doesn't exist and  
// you should implement your own as you may want to interpret it differently  
// i.e. based on the current principal  
var errorDetail = context.Request.IsTrusted()  
? exception.Demystify().ToString()  
: "The instance value should be used to identify the problem when calling customer support";

var problemDetails = new ProblemDetails  
{  
Title = "An unexpected error occurred!",  
Status = 500,  
Detail = errorDetail,  
Instance = $"urn:myorganization:error:{Guid.NewGuid()}"  
};

// log the exception etc..  
// flush problemDetails to the caller  
});  
});

The IsTrusted() is a hypothetical extension method that you could create for yourself to determine whether or not you want to share exception details with the caller. I also used Ben’s excellent demystifier library to process and format the exception nicely into a string. As Instance we use a generic organizational URI, combined with GUID.

In order to flush the response, we use a helper extension method. As a reminder - the Type property is going to be omitted by design, so we will use the setting NullValueHandling.Ignore in the JSON serializer.

app.UseExceptionHandler(errorApp =>  
{  
errorApp.Run(async context =>  
{  
var errorFeature = context.Features.Get<IExceptionHandlerFeature>();  
var exception = errorFeature.Error;

// the IsTrusted() extension method doesn't exist and  
// you should implement your own as you may want to interpret it differently  
// i.e. based on the current principal  
var errorDetail = context.Request.IsTrusted()  
? exception.Demystify().ToString()  
: "The instance value should be used to identify the problem when calling customer support";

var problemDetails = new ProblemDetails  
{  
Title = "An unexpected error occurred!",  
Status = 500,  
Detail = errorDetail,  
Instance = $"urn:myorganization:error:{Guid.NewGuid()}"  
};

// log the exception etc..

context.Response.WriteJson(problemDetails, "application/problem+json");  
});  
});

public static class HttpExtensions  
{  
private static readonly JsonSerializer Serializer = new JsonSerializer  
{  
NullValueHandling = NullValueHandling.Ignore  
};

public static void WriteJson<T>(this HttpResponse response, T obj, string contentType = null)  
{  
response.ContentType = contentType ?? "application/json";  
using (var writer = new HttpResponseStreamWriter(response.Body, Encoding.UTF8))  
{  
using (var jsonWriter = new JsonTextWriter(writer))  
{  
jsonWriter.CloseOutput = false;  
jsonWriter.AutoCompleteOnClose = false;

Serializer.Serialize(jsonWriter, obj);  
}  
}  
}  
}

We are very close now, as we are finally flushing the response to the caller. We could start testing this with some exceptions being thrown in our application, and bubbling up all the way to the surface - they should be converted into this nice standardized error format.

One extra thing we still need to handle, are special “bad request” Kestrel exceptions. These are related to low-server-level issues with the request, for example an HTTP method not being allowed at server level, too large headers or even too large payload body (which, by the way, is configurable, and commonly used to prevent i.e. too large files being uploaded). In those cases, Kestrel will throw BadHttpRequestException which will pass through our error handler. The problem is we shouldn’t convert it into a 500, like we do at the moment, because those errors are not 500s - for example a request body that is too large should result in HTTP Status Code 413.

The solution is to special case our handler to be able to deal with those Kestrel’s BadHttpRequestException. It will unfortunately require us to reach into some reflection tricks, as certain crucial information is internal only.

Here is our, slightly restructured, final version of the error handler:

app.UseExceptionHandler(errorApp =>  
{  
errorApp.Run(async context =>  
{  
var errorFeature = context.Features.Get<IExceptionHandlerFeature>();  
var exception = errorFeature.Error;

// the IsTrusted() extension method doesn't exist and  
// you should implement your own as you may want to interpret it differently  
// i.e. based on the current principal

var problemDetails = new ProblemDetails  
{  
Instance = $"urn:myorganization:error:{Guid.NewGuid()}"  
};

if (exception is BadHttpRequestException badHttpRequestException)  
{  
problemDetails.Title = "Invalid request";  
problemDetails.Status = (int)typeof(BadHttpRequestException).GetProperty("StatusCode",  
BindingFlags.NonPublic | BindingFlags.Instance).GetValue(badHttpRequestException);  
problemDetails.Detail = badHttpRequestException.Message;  
}  
else  
{  
problemDetails.Title = "An unexpected error occurred!";  
problemDetails.Status = 500;  
problemDetails.Detail = exception.Demystify().ToString();  
}

// log the exception etc..

context.Response.StatusCode = problemDetails.Status.Value;  
context.Response.WriteJson(problemDetails, "application/problem+json");  
});  
});

The end result is the following - if we have a “regular” unhandled exception from our code, the returned payload looks like this (provided you are allowed to see the exception details):

{  
"Title": "An unexpected error occurred!",  
"Status": 500,  
"Detail": "System.Exception: something bad happened! at ActionResult<IEnumerable<string>> WebApplication4.Controllers.ValuesController.Get() in c:/users/filip/source/repos/WebApplication4/Controllers/ValuesController.cs:line 17 at object lambda_method(Closure, object, object[]) at object Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(object target, object[] parameters) at ValueTask<IActionResult> Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor+SyncObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, object controller, object[] arguments) at async Task Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync() at async Task Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync() at void Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context) at Task Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted) at async Task Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync() at async Task Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter() at void Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context) at Task Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted) at async Task Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync() at async Task Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync() at async Task Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext) at async Task Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)",  
"Instance": "urn:myorganization:error:bc7d0b68-733c-4db4-84d8-d1954b3e5550"  
}

For the Kestrel request validation error, the response looks like this:

{  
"Title": "Invalid request",  
"Status": 413,  
"Detail": "Request body too large.",  
"Instance": "urn:myorganization:error:91591d13-40d7-413d-87eb-02591b1f92a4"  
}

Input validation πŸ”—

So we have configured everything that is needed for error handling. The one thing that we haven’t tackled yet is doing request validation. That is, for example, doing the regular validation of the payload submitted to our API. This is definitely something we should look at - as a standardized Problem Details response would make a lot of sense here. After all, input validation failures are probably a lot more common than unhandled exceptions.

The best way to approach this would be to create a dedicated IActionResult, which we can return in case a malformed (bad) request is submitted by the caller to our API. However, before we do it, a few words about extending the ProblemDetails, because as we have seen, it is nice and standardized, but also a bit constraining, if we just consider its few basic properties.

The RFC actually allows for additional members (“extension members”) to be defined alongside the standard properties. Clients consuming the response are explicitly required to ignore the members it can’t understand allowing the possibility for the payload to additively evolve over time.

Imagine that we’d like to have an additional property related to our request validation errors. The MVC framework already captures validation errors into a ModelStateDictionary, so we could project information from there into our extended ProblemDetails. The outline of that is shown in the next snippet:

public class ValidationProblemDetails : ProblemDetails  
{  
public ICollection<ValidationError> ValidationErrors { get; set; }  
}

public class ValidationError  
{  
public string Name { get; set; }  
public string Description { get; set; }  
}

We will be projecting the MVC framework’s ModelStateDictionary into a collection of ValidationError to simplify presentation. With all that in place, we can proceed towards setting up our custom IActionResult, the ValidationProblemDetails, which is shown next.

public class ValidationProblemDetailsResult : IActionResult  
{  
public Task ExecuteResultAsync(ActionContext context)  
{  
var modelStateEntries = context.ModelState.Where(e => e.Value.Errors.Count > 0).ToArray();  
var errors = new List<ValidationError>();

var details = "See ValidationErrors for details";

if (modelStateEntries.Any())  
{  
if (modelStateEntries.Length == 1 && modelStateEntries[0].Value.Errors.Count == 1 && modelStateEntries[0].Key == string.Empty)  
{  
details = modelStateEntries[0].Value.Errors[0].ErrorMessage;  
}  
else  
{  
foreach (var modelStateEntry in modelStateEntries)  
{  
foreach (var modelStateError in modelStateEntry.Value.Errors)  
{  
var error = new ValidationError  
{  
Name = modelStateEntry.Key,  
Description = modelStateError.ErrorMessage  
};

errors.Add(error);  
}  
}  
}  
}

var problemDetails = new ValidationProblemDetails  
{  
Status = 400,  
Title = "Request Validation Error",  
Instance = $"urn:myorganization:badrequest:{Guid.NewGuid()}",  
Detail = details,  
ValidationErrors = errors  
};

context.HttpContext.Response.WriteJson(problemDetails);  
return Task.CompletedTask;  
}  
}

As our action result is executed, we can reach into the ModelState and extract the relevant errors. There is a bit of custom logic there, which we don’t necessarily need to go into that deeply. The gist of it is that if the whole request has some fundamental problems (for example a JSON body is missing at all), then ModelState would normally contain a single error entry, that is keyed against an empty string. In that case, we want to convey that error to the caller in the Detail property of our ValidationProblemDetails. When that happens, the ValidationErrors property will be an empty array, and that is OK.

In all other cases, we simply pick up the errors from the model state dictionary and put them into the ValidationErrors property. In that case, the Detail message of our response is going to be a generic text, something like “See ValidationErrors for details”.

In either situation, the value of Instance, is, similarly to our earlier efforts, a URN - formatted accordingly: “urn:myorganization:badrequest:{Guid.NewGuid()}”. The Status is always 400 and the Title is also a static “Request Validation Error”.

The question now, is how to actually best consume this action result? The easiest way would be to just manually return ValidationProblemDetails from the action. Let’s imagine having a following model:

public class Item  
{  
[Required]  
public string Name { get; set; }

[Range(1, 10)]  
public int Rating { get; set; }  
}

We could, for example, do the inline ModelState validity check and return the ValidationProblemDetails manually:

[HttpPost]  
public IActionResult Post([FromBody]Item value)  
{  
if (!ModelState.IsValid)  
{  
return new ValidationProblemDetails();  
}

// process the "happy path"  
}

If someone, for example, skips the Name property or uses a Rating value outside of the defined range, then the caller will get a relevant Problem Details response.

Such usage pattern is IMHO not very elegant, but it would work. Probably a better way would be to incorporate our ValidationProblemDetails into the ApiControllerAttribute feature of ASP.NET Core 2.1. The way it works, is that if we put an [ApiController] attribute on our controller, a bunch of nice things will happen, including automatic validation of model state. This is great, but of course if the model state validation happens automatically, and the response is automatically produced by the framework, how can we make our ValidationProblemDetails part of that?

Turns out you can register a custom IActonResult factory that will be used in case of validation failures, which is exactly what we need. This is done in the Startup class.

public void ConfigureServices(IServiceCollection services)  
{  
services.AddMvc();  
services.Configure<ApiBehaviorOptions>(options =>  
{  
options.InvalidModelStateResponseFactory = ctx => new ValidationProblemDetailsResult();  
});  
}

With that in place, we can no write our controllers in a very clean way, with only the so called “happy path” inline in the actions, as all of the stuff related to request validation is centrally managed and handled by the combination of the [ApiController] functionality and our ValidationProblemDetails.

[Route("api/[controller]")]  
[ApiController]  
public class ItemsController : ControllerBase  
{  
[HttpPost]  
public void Post(Item value)  
{  
// happy path only, invalid requests never reach this point  
}  
}  

For the record, this is how our response would look like, if the request is completely malformed (for example a 0 length body is sent by the caller):

{  
"Title": "Request Validation Error",  
"Status": 400,  
"Detail": "A non-empty request body is required.",  
"Instance": "urn:myorganization:badrequest:dde60a57-1750-4bd9-9ac3-daa7ddba4b9a",  
"ValidationErrors": []  
}

Now, in case of invalid contents of the JSON payload, the model state validation kicks in, and the individual details are convey via our extension property.

{  
"Title": "Request Validation Error",  
"Status": 400,  
"Detail": "See ValidationErrors for details",  
"Instance": "urn:myorganization:badrequest:d2b5afe4-8047-45fc-a6aa-9761ba8e4850",  
"ValidationErrors":  
[  
{  
"Name": "Name",  
"Description": "The Name field is required."  
},  
{  
"Name": "Rating",  
"Description": "The field Rating must be between 1 and 10."  
}  
]  
}

Summary πŸ”—

And this is it. Obviously you can take this concept even further - I only wanted to show 2 common scenarios today, both of which are designed around centralized handling of matters - dealing with unhandled exceptions and dealing with request validation.

In real life, you will probably have some extra validation requirements that may not map very easily to the concept of ModelState and its projection onto our general purpose responses. In such situations, you’d like be subclassing ProblemDetails on your own and building your own customized IActionResult to handle that.

The same applies for having customized status codes - it’s not difficult to envision use cases for status codes like 409 (Conflict) or 410 (Gone). You could even enrich 404 payloads with the “Problem Details” approach.

However, I hope this article will help you get started, and nudge you in a right direction.

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP