Couple of days ago, we had an outage in one of our production APIs that’s built with ASP.NET Core 2.0 and all we got to see in all our logging was something similar to this which doesn’t really help reveal the root cause:
After much debugging hassle for several hours we found that it was caused by a type mismatch between the DTO on the server (which expected an
int) and client sent JSON payload to the POST endpoint (which sent a
decimal), in one of the fields. This resulted in the following exception:
Newtonsoft.Json.JsonReaderException: Input string ‘20.4’ is not a valid integer. Path ‘Age’, line 3, position 12.
It turns out that ASP.NET Core 2.0 by default doesn’t report back on model validation errors but continues processing the invalid request resulting in the 500 ISE error.
It also turns out that ASP.NET Core 2.2 has already solved this problem by sending back a 400 Bad Request when model validation fails thereby short circuiting the rest of the request and reporting back the real issue:
The obvious solution therefore might seem like upgrading to ASP.NET Core 2.2 but sometimes due to organisational constraints its not as easy. So let’s assume that is not on the table for now.
Then how do I add similar model validation error reporting to the 2.0 solution? First, lets look at how 2.2 does it.
To deal with model validation error reporting, in ASP.NET Core 2.2, 2 new types were added:
- ValidationProblemDetails which inherits from ProblemDetails and deals specifically with model validation errors.
During a model validation failure event, an instance of ValidationProblemDetails is filled in and sent out with
Content-Type header set to
application/problem+json. This header value is an RFC standard for HTTP APIs that want to send out human/machine readable responses when request processing fails, giving plenty of context and information for easier diagnostics.
Back in my ASP.NET Core 2.0 API, where these types aren’t available, I will create them manually and customise them slightly for my purposes. First up,
In my version of this class, I am returning all the inner exceptions as well as the stack trace and I’ve created an extension method on the
Exception class to do so:
Once this is in place, I will create an
IActionFilter that can allow me to short circuit the request pipeline if my model state is not valid and send an appropriate response with model validation errors:
Using action filters is a great way to take care of cross cutting concerns like logging, model validation error reporting, setting up of any ambient contexts etc in a central place instead of duplicating this code in each controller action which can lead to divergence in behaviour over time.
Because RFC recommends so, I will also set the
Content-Type header to
application/problem+json and this way the client knows that this response contains an error report.
NB: I can also log the model errors in the filter for later diagnosis (for this post though I haven’t shown it). The mere act of setting the
context.Result will short circuit the rest of the pipeline and will prevent the generic and misleading
500 Internal Server Error. I say misleading because ultimately, its the request that’s not valid and that makes it a
400 Bad Request, the fact that it causes a null object and using that null object subsequently causes a request failure, is a symptom of this bad request and not the cause.
Finally, I will simply add this filter to my MVC pipeline:
Now when I make the request again with my dodgy JSON payload, I get this response:
Now this is what I would have expected to see in every ASP.NET Core version out of the box, would have saved us several hours of debugging if we’d seen this in our logs! Why it only appeared after 2.0, I have no idea!
I can also create a simpler version of the action filter, one that doesn’t use the ProblemDetails class hierarchy at all but simply queries the model state and sends back the errors. Something like this:
Resulting in a response that looks like this:
If you want a quick and cheap approach then this simpler variant might be just fine but if you want to follow the whole RFC-esque problem details style pattern because it will make the transition to ASP.NET Core 2.2 easier, then go with the first version.
Anyway, the PoC code for this post is up on my GitHub! Thanks!