Is this a Bug or Feature request?:
Feature Request / Question
Steps to reproduce or link to a repro project:
[ApiController] [Produces("application/json")] [Route("api/v1/value")] public class ValueApiController : Controller { [HttpGet] public ActionResult<string[]> Get() { throw new Exception("bam"); } }
Startup.cs
if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); app.UseStatusCodePagesWithReExecute("/Error"); }
Description of the problem:
When an error occurs in Controller classes marked with [ApiController]
, I don’t want them to return HTML but JSON instead:
-
In development, instead of Developer Exception Page, ideally the API Controller should return stack trace in plain text / JSON.
-
In production, instead of a custom HTML error response, ideally the API Controller should return a plain text / JSON with like
"An error occurred while processing your request."
When using Core MVC < 2.1, I have been able to do this by using code like:
app.UseWhen(context => context.Request.Path.Value.StartsWith("/api", StringComparison.OrdinalIgnoreCase), builder => { builder.UseExceptionHandler(configure => { configure.UseMiddleware<ApiExceptionHandlerMiddleware>(env.IsDevelopment()); }); }); app.UseWhen(context => context.Request.Path.Value.StartsWith("/api", StringComparison.OrdinalIgnoreCase) == false, builder => { if (env.IsDevelopment()) { builder.UseDeveloperExceptionPage(); } else { builder.UseExceptionHandler("/error"); builder.UseStatusCodePagesWithReExecute("/error"); } });
However, seeing that the purpose of the [ApiController]
is: (written on the XML doc)
Indicates that a type and all derived types are used to serve HTTP API responses. The presence of this attribute can be used to target conventions, filters and other behaviors based on the purpose of the controller.
, I would like to know how to alter the error behavior when this attribute is encountered. (Instead of checking whether the request starts with /api
)
Version of Microsoft.AspNetCore.Mvc
or Microsoft.AspNetCore.App
or Microsoft.AspNetCore.All
:
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0-rc1-final" />
Annotating the controllers with ApiController
attribute in ASP.NET Core 2.1 or higher will enable the behavioral options for the API’s. These behavioral options include automatic HTTP 400 responses as well.
In this post, we’ll see how we can customize the default error response from the ASP.NET Core Web API.
Default error response
If you are creating a new default ASP.NET Core web API project, then you’d see the ValuesController.cs
file in the project. Otherwise, create a Controller and create an action method to a parameter to test the automatic HTTP 400 responses.
If you are creating a custom API for yourself you’d need to annotate the controller with [ApiController]
attribute. Otherwise, the default 400 responses won’t work.
I’ll go with the default ValuesController
for now. We already have the Get
action with id passed in as the parameter.
// GET api/values/5 [HttpGet(“{id}”)] public ActionResult Get(int id) { return “value”; }
Let’s try to pass in a string for the id parameter for the Get action through Postman.
This is the default error response returned by the API.
Notice that we didn’t have the ModelState checking in our Get
action method this is because ASP.NET Core did it for us as we have the [ApiController]
attribute on top of our controller.
To modify the error response we need to make use of the InvalidModelStateResponseFactory property.
InvalidModelStateResponseFactory
is a delegate which will invoke the actions annotated with ApiControllerAttribute to convert invalid ModelStateDictionary into an IActionResult.
The default response type for HTTP 400 responses is ValidationProblemDetails class. So, we will create a custom class which inherits ValidationProblemDetails
class and define our custom error messages.
CustomBadRequest class
Here is our CustomBadRequest
class which assigns error properties in the constructor.
public class CustomBadRequest : ValidationProblemDetails { public CustomBadRequest(ActionContext context) { Title = “Invalid arguments to the API”; Detail = “The inputs supplied to the API are invalid”; Status = 400; ConstructErrorMessages(context); Type = context.HttpContext.TraceIdentifier; } private void ConstructErrorMessages(ActionContext context) { foreach (var keyModelStatePair in context.ModelState) { var key = keyModelStatePair.Key; var errors = keyModelStatePair.Value.Errors; if (errors != null && errors.Count > 0) { if (errors.Count == 1) { var errorMessage = GetErrorMessage(errors[0]); Errors.Add(key, new[] { errorMessage }); } else { var errorMessages = new string[errors.Count]; for (var i = 0; i < errors.Count; i++) { errorMessages[i] = GetErrorMessage(errors[i]); } Errors.Add(key, errorMessages); } } } } string GetErrorMessage(ModelError error) { return string.IsNullOrEmpty(error.ErrorMessage) ? “The input was not valid.” : error.ErrorMessage; } }
I’m using ActionContext as a constructor argument as we can have more information about the action. I’ve used the ActionContext as I’m using the TraceIdentifier
from the HttpContext
. The Action context will have route information, HttpContext, ModelState, ActionDescriptor.
You could pass in just the model state in the action context at least for this bad request customization. It is up to you.
Plugging the CustomBadRequest in the configuration
We can configure our newly created CustomBadRequest in Configure method in Startup.cs
class in two different ways.
using ConfigureApiBehaviorOptions off AddMvc()
ConfigureApiBehaviorOptions
is an extension method on IMvcBuilder
interface. Any method that returns an IMvcBuilder
can call the ConfigureApiBehaviorOptions
method.
public static IMvcBuilder ConfigureApiBehaviorOptions(this IMvcBuilder builder, Action setupAction);
The AddMvc()
returns an IMvcBuilder
and we will plug our custom bad request here.
services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_2) .ConfigureApiBehaviorOptions(options => { options.InvalidModelStateResponseFactory = context => { var problems = new CustomBadRequest(context); return new BadRequestObjectResult(problems); }; });
using the generic Configure method
This will be convenient as we don’t chain the configuration here. We’ll be just using the generic configure method here.
services.Configure(a => { a.InvalidModelStateResponseFactory = context => { var problemDetails = new CustomBadRequest(context); return new BadRequestObjectResult(problemDetails) { ContentTypes = { “application/problem+json”, “application/problem+xml” } }; }; });
Testing the custom bad request
That’s it for configuring the custom bad request, let’s run the app now.
You can see the customized HTTP 400 error messages we’ve set in our custom bad request class showing up.
Image Credits
- Featured Image by Sandrachile from Unsplash.com
Hello there. I’m a passionate Full Stack developer and I work primarily on .NET Core, microservices, distributed systems, VUE, JavaScript
Sometimes we just want to add some extra validation to a bound model within the body of the action within a controller, it’s the most simplest approach to adding some custom validation to your models without going overboard.
You’d think this would be fairly simple but we’ll soon see that it doesn’t send the same response as the framework by just calling
return BadRequest(ModelState);
Invalid Models
The ASP.NET Core framework is really helpful, most of the handling of invalid models is done for us by the framework.
Take the following code for example.
[Route("api/values")]
[ApiController]
public class ValuesController : Controller
{
// GET: api/values?from=2020-01-01&to=2020-01-31
[HttpGet]
public IActionResult Get([FromQuery] GetValuesQueryParameters parameters)
=> Ok(new
{
parameters.From,
parameters.To
});
public class GetValuesQueryParameters
{
[Required] public DateTime? From { get; set; }
[Required] public DateTime? To { get; set; }
}
}
Our GetValuesQueryParameters
model has a couple of [Required]
attributes on it, this tells the framework these are required properties to progress the request. There are loads of different validation attributes that you can apply, you can check out the comprehensive list on the documentation site.
You might have also noticed we have got an attribute of [ApiController]
on the controller, this tells the framework to apply the api behaviors, one of these is to automatically check if there are any errors on the ModelState and if there is, it will return a 400 bad request.
The response that we get back from the api from calling /api/values
with no query string will be:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|3184ae60-44f89c2239f987a2.",
"errors": {
"To": [
"The To field is required."
],
"From": [
"The From field is required."
]
}
}
As you can see it’s nice and descriptive and even includes a trace id!
Extending Validation in Controller Action
Say we want to extend the validation in our controller action, for this example we will make sure that our date ranges are no more than 31 days apart.
We will check the date ranges and then add a model error to the ModelState
with the given property and then return a BadRequest
with the ModelState
.
[HttpGet]
public IActionResult Get([FromQuery] GetValuesQueryParameters parameters)
{
if ((parameters.To!.Value - parameters.From!.Value).TotalDays > 31)
{
ModelState.AddModelError(nameof(GetValuesQueryParameters.To), "The date range for the query can be maximum of 31 days.");
return BadRequest(ModelState);
}
return Ok(new
{
parameters.From,
parameters.To
});
}
Now if we make a GET request to the url /api/values?from=2020-01-01&to=2020-12-01
we’ll receive a 400
bad request response back with the following body:
{
"From": [
"The date range for the query can be maximum of 31 days."
]
}
So our extra bit of validation is now running and we’re getting the right response code but the body of the response is completely different from what the ASP.NET Core framework was giving us originally.
Returning The Same Response Body
It would be nice to keep the response the same as what the framework was giving us originally, to do that, we need to injecting in ApiBehaviorOptions
in to our action, these options are used to describe how the api should behavior. One of the options is a factory to create the response back from the api when the model state is invalid, this is called InvalidModelStateResponseFactory
. We can call this factory with the ControllerContext
which will give us back an IActionResult
in which we can return back to the action.
[HttpGet]
public IActionResult Get(
[FromQuery] GetValuesQueryParameters parameters,
[FromServices] IOptions<ApiBehaviorOptions> apiBehaviorOptions)
{
if ((parameters.To!.Value - parameters.From!.Value).TotalDays > 31)
{
ModelState.AddModelError(nameof(GetValuesQueryParameters.To), "The date range for the query can be maximum of 31 days.");
return apiBehaviorOptions.Value.InvalidModelStateResponseFactory(ControllerContext);
}
return Ok(new { parameters.From, parameters.To });
}
Now if we do another GET request to the same url /api/values?from=2020-01-01&to=2020-12-01
we will get the same response as originally from the framework:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|b587c9f9-4aff6eb0721c184a.",
"errors": {
"To": [
"The date range for the query can be maximum of 31 days."
]
}
}
Customizing The Model Validation Response.
Now we know that there is a factory for creating the response from an invalid model state, we can replace the factory with our own factory to create custom responses.
Within the Startup.cs
in the ConfigureServices
function, after the AddMvc
call, we can chain an extra method call of ConfigureApiBehaviorOptions
this is where we can alter the options.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.ConfigureApiBehaviorOptions(opt
=>
{
opt.InvalidModelStateResponseFactory =
(context => new OkObjectResult("Hello there?"));
});
}
Now if we spin back up the api and get an invalid model state, we’ll get the following response from the api.
Respect the API Behavior Options
From this we now should see that we should respect the API behavior options within our controller, that way if we ever wanted to globally change how the invalid model state responses are create, we only have one place to change it.
When I talk about exceptions in my product team I often talk about two kind of exceptions, business and critical exceptions. Business exceptions are exceptions thrown based on “business rules”, for example if you aren’t allowed to do a purchase. Business exceptions in most case aren’t important to log into a log file, they can directly be shown to the user. An example of a business exception could be «DeniedToPurchaseException”, or some validation exceptions such as “FirstNameIsMissingException” etc.
Critical Exceptions are all other kind of exceptions such as the SQL server is down etc. Those kind of exception message need to be logged and should not reach the user, because they can contain information that can be harmful if it reach out to wrong kind of users.
I often distinguish business exceptions from critical exceptions by creating a base class called BusinessException, then in my error handling code I catch on the type BusinessException and all other exceptions will be handled as critical exceptions.
This blog post will be about different ways to handle exceptions and how Business and Critical Exceptions could be handled.
Web API and Exceptions the basics
When an exception is thrown in a ApiController a response message will be returned with a status code set to 500 and a response formatted by the formatters based on the “Accept” or “Content-Type” HTTP header, for example JSON or XML. Here is an example:
public IEnumerable<string> Get() { throw new ApplicationException("Error!!!!!"); return new string[] { "value1", "value2" }; }
The response message will be:
HTTP/1.1 500 Internal Server Error Content-Length: 860 Content-Type: application/json; charset=utf-8 { "ExceptionType":"System.ApplicationException","Message":"Error!!!!!","StackTrace":" at ..."}
The stack trace will be returned to the client, this is because of making it easier to debug. Be careful so you don’t leak out some sensitive information to the client. So as long as you are developing your API, this is not harmful. In a production environment it can be better to log exceptions and return a user friendly exception instead of the original exception.
There is a specific exception shipped with ASP.NET Web API that will not use the formatters based on the “Accept” or “Content-Type” HTTP header, it is the exception is the HttpResponseException class.
Here is an example where the HttpReponseExcetpion is used:
// GET api/values [ExceptionHandling] public IEnumerable<string> Get() { throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)); return new string[] { "value1", "value2" }; }
The response will not contain any content, only header information and the status code based on the HttpStatusCode passed as an argument to the HttpResponseMessage. Because the HttpResponsException takes a HttpResponseMessage as an argument, we can give the response a content:
public IEnumerable<string> Get() { throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent("My Error Message"), ReasonPhrase = "Critical Exception" }); return new string[] { "value1", "value2" }; }
The code above will have the following response:
HTTP/1.1 500 Critical Exception Content-Length: 5 Content-Type: text/plain; charset=utf-8 My Error Message
The Content property of the HttpResponseMessage doesn’t need to be just plain text, it can also be other formats, for example JSON, XML etc.
By using the HttpResponseException we can for example catch an exception and throw a user friendly exception instead:
public IEnumerable<string> Get() { try { DoSomething(); return new string[] { "value1", "value2" }; } catch (Exception e) { throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent("An error occurred, please try again or contact the administrator."), ReasonPhrase = "Critical Exception" }); } }
Adding a try catch to every ApiController methods will only end in duplication of code, by using a custom ExceptionFilterAttribute or our own custom ApiController base class we can reduce code duplicationof code and also have a more general exception handler for our ApiControllers . By creating a custom ApiController’s and override the ExecuteAsync method, we can add a try catch around the base.ExecuteAsync method, but I prefer to skip the creation of a own custom ApiController, better to use a solution that require few files to be modified.
The ExceptionFilterAttribute has a OnException method that we can override and add our exception handling. Here is an example:
using System; using System.Diagnostics; using System.Net; using System.Net.Http; using System.Web.Http; using System.Web.Http.Filters; public class ExceptionHandlingAttribute : ExceptionFilterAttribute { public override void OnException(HttpActionExecutedContext context) { if (context.Exception is BusinessException) { throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent(context.Exception.Message), ReasonPhrase = "Exception" }); }
//Log Critical errors
Debug.WriteLine(context.Exception); throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent("An error occurred, please try again or contact the administrator."), ReasonPhrase = "Critical Exception" }); } }
Note: Something to have in mind is that the ExceptionFilterAttribute will be ignored if the ApiController action method throws a HttpResponseException.
The code above will always make sure a HttpResponseExceptions will be returned, it will also make sure the critical exceptions will show a more user friendly message. The OnException method can also be used to log exceptions.
By using a ExceptionFilterAttribute the Get() method in the previous example can now look like this:
public IEnumerable<string> Get() { DoSomething(); return new string[] { "value1", "value2" }; }
To use the an ExceptionFilterAttribute, we can for example add the ExceptionFilterAttribute to our ApiControllers methods or to the ApiController class definition, or register it globally for all ApiControllers. You can read more about is here.
Note: If something goes wrong in the ExceptionFilterAttribute and an exception is thrown that is not of type HttpResponseException, a formatted exception will be thrown with stack trace etc to the client.
How about using a custom IHttpActionInvoker?
We can create our own IHTTPActionInvoker and add Exception handling to the invoker. The IHttpActionInvoker will be used to invoke the ApiController’s ExecuteAsync method. Here is an example where the default IHttpActionInvoker, ApiControllerActionInvoker, is used to add exception handling:
public class MyApiControllerActionInvoker : ApiControllerActionInvoker { public override Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, System.Threading.CancellationToken cancellationToken) { var result = base.InvokeActionAsync(actionContext, cancellationToken); if (result.Exception != null && result.Exception.GetBaseException() != null) { var baseException = result.Exception.GetBaseException(); if (baseException is BusinessException) { return Task.Run<HttpResponseMessage>(() => new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent(baseException.Message), ReasonPhrase = "Error" }); } else { //Log critical error Debug.WriteLine(baseException); return Task.Run<HttpResponseMessage>(() => new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent(baseException.Message), ReasonPhrase = "Critical Error" }); } } return result; } }
You can register the IHttpActionInvoker with your own IoC to resolve the MyApiContollerActionInvoker, or add it in the Global.asax:
GlobalConfiguration.Configuration.Services.Remove(typeof(IHttpActionInvoker), GlobalConfiguration.Configuration.Services.GetActionInvoker()); GlobalConfiguration.Configuration.Services.Add(typeof(IHttpActionInvoker), new MyApiControllerActionInvoker());
How about using a Message Handler for Exception Handling?
By creating a custom Message Handler, we can handle error after the ApiController and the ExceptionFilterAttribute is invoked and in that way create a global exception handler, BUT, the only thing we can take a look at is the HttpResponseMessage, we can’t add a try catch around the Message Handler’s SendAsync method. The last Message Handler that will be used in the Wep API pipe-line is the HttpControllerDispatcher and this Message Handler is added to the HttpServer in an early stage. The HttpControllerDispatcher will use the IHttpActionInvoker to invoke the ApiController method. The HttpControllerDipatcher has a try catch that will turn ALL exceptions into a HttpResponseMessage, so that is the reason why a try catch around the SendAsync in a custom Message Handler want help us. If we create our own Host for the Wep API we could create our own custom HttpControllerDispatcher and add or exception handler to that class, but that would be little tricky but is possible.
We can in a Message Handler take a look at the HttpResponseMessage’s IsSuccessStatusCode property to see if the request has failed and if we throw the HttpResponseException in our ApiControllers, we could use the HttpResponseException and give it a Reason Phrase and use that to identify business exceptions or critical exceptions.
I wouldn’t add an exception handler into a Message Handler, instead I should use the ExceptionFilterAttribute and register it globally for all ApiControllers. BUT, now to another interesting issue. What will happen if we have a Message Handler that throws an exception? Those exceptions will not be catch and handled by the ExceptionFilterAttribute.
I found a bug in my previews blog post about “Log message Request and Response in ASP.NET WebAPI” in the MessageHandler I use to log incoming and outgoing messages. Here is the code from my blog before I fixed the bug:
public abstract class MessageHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var corrId = string.Format("{0}{1}", DateTime.Now.Ticks, Thread.CurrentThread.ManagedThreadId); var requestInfo = string.Format("{0} {1}", request.Method, request.RequestUri); var requestMessage = await request.Content.ReadAsByteArrayAsync(); await IncommingMessageAsync(corrId, requestInfo, requestMessage); var response = await base.SendAsync(request, cancellationToken); var responseMessage = await response.Content.ReadAsByteArrayAsync(); await OutgoingMessageAsync(corrId, requestInfo, responseMessage); return response; } protected abstract Task IncommingMessageAsync(string correlationId, string requestInfo, byte[] message); protected abstract Task OutgoingMessageAsync(string correlationId, string requestInfo, byte[] message); }
If a ApiController throws a HttpResponseException, the Content property of the HttpResponseMessage from the SendAsync will be NULL. So a null reference exception is thrown within the MessageHandler. The yellow screen of death will be returned to the client, and the content is HTML and the Http status code is 500. The bug in the MessageHandler was solved by adding a check against the HttpResponseMessage’s IsSuccessStatusCode property:
public abstract class MessageHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var corrId = string.Format("{0}{1}", DateTime.Now.Ticks, Thread.CurrentThread.ManagedThreadId); var requestInfo = string.Format("{0} {1}", request.Method, request.RequestUri); var requestMessage = await request.Content.ReadAsByteArrayAsync(); await IncommingMessageAsync(corrId, requestInfo, requestMessage); var response = await base.SendAsync(request, cancellationToken); byte[] responseMessage; if (response.IsSuccessStatusCode) responseMessage = await response.Content.ReadAsByteArrayAsync(); else responseMessage = Encoding.UTF8.GetBytes(response.ReasonPhrase); await OutgoingMessageAsync(corrId, requestInfo, responseMessage); return response; } protected abstract Task IncommingMessageAsync(string correlationId, string requestInfo, byte[] message); protected abstract Task OutgoingMessageAsync(string correlationId, string requestInfo, byte[] message); }
If we don’t handle the exceptions that can occur in a custom Message Handler, we can have a hard time to find the problem causing the exception. The savior in this case is the Global.asax’s Application_Error:
protected void Application_Error() { var exception = Server.GetLastError(); Debug.WriteLine(exception); }
I would recommend you to add the Application_Error to the Global.asax and log all exceptions to make sure all kind of exception is handled.
Summary
There are different ways we could add Exception Handling to the Wep API, we can use a custom ApiController, ExceptionFilterAttribute, IHttpActionInvoker or Message Handler. The ExceptionFilterAttribute would be a good place to add a global exception handling, require very few modification, just register it globally for all ApiControllers, even the IHttpActionInvoker can be used to minimize the modifications of files. Adding the Application_Error to the global.asax is a good way to catch all unhandled exception that can occur, for example exception thrown in a Message Handler.
If you want to know when I have posted a blog post, you can follow me on twitter @fredrikn
A brief introduction of Problem details
If you have developed HTTP APIs, I’m sure that many times you have had the need to define new error response formats for HTTP APIs to return to your clients. The most common way to do this is using HTTP Status Codes but sometimes is not sufficient to communicate enough information to the clients. For example, imagine a banking HTTP API that allows customers to make online transactions and this call returns Forbidden (403) response telling the client that the customer isn’t allowed to make this transaction, but why? Maybe the customer doesn’t have enough credit? or he has a maximum limit of money to transfer?
To provide additional information to our clients we would need to extend the response body with some kind of document (JSON or XML). The problem I’ve seen in many HTTP APIs is that usually these documents are not the same. It’s very frustating for a client that consumes many HTTP APIs because there isn’t a standard way to deal with these errors and it would need to implement different ways to work with them.
Due to the need to standarize an error response format for HTTP APIs, the Internet Engineering Task Force (IETF) published in March 2016 a document that defines a “problem details” 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 (Not reinvent the wheel).
Let me show you an example of HTTP response of JSON problem details
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": ["/account/12345","/account/67890"],
"status": 403
}
The format of the message is application/problem+json media type (It could be application/problem+xml also) and we have several members in the response body:
- type (string): URI that identifies the problem detail type. In this case “out-of-credit”.
- title (string): A short human-readable summary about the problem.
- detail (string): A human-readable explanation about the problem.
- status (number): HTTP Status Code.
- instance (string): A URI reference that identifies the specific occurrence of the problem.
We can extend the problem details object with additional members, for example the previous message defines two members “balance” and “accounts” to communicate additional information to the client.
Problem details in ASP.NET Core 2.1
When ASP.NET Core 2.1 came out, the ASP.NET Core team added support for problem details. The class that represents a problem details object is ProblemDetails
An example of use:
[HttpPost]
public ActionResult Transfer()
{
try
{
/// Make a transfer
}
catch (OutOfCreditException ex)
{
var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status403Forbidden,
Type = "https://example.com/probs/out-of-credit",
Title = "You do not have enough credit.",
Detail = "Your current balance is 30, but that costs 50.",
Instance = HttpContext.Request.Path
};
return new ObjectResult(problemDetails)
{
ContentTypes = { "application/problem+json" },
StatusCode = 403,
};
}
return Ok();
}
We alse have the ValidationProblemDetails class for validation errors. This class inherits from ProblemDetails and you can see an example in the following code:
[HttpPost]
public ActionResult Transfer(TransferInfo model)
{
if (!ModelState.IsValid)
{
var problemDetails = new ValidationProblemDetails(ModelState);
return new ObjectResult(problemDetails)
{
ContentTypes = { "application/problem+json" },
StatusCode = 403,
};
}
return Ok();
}
The two previous examples have the same problem: They are polluting our controllers (Put your controllers on a diet). IMHO controllers act as mediators: receive the request from the client, transform into a command, send it and response to the client. Let’s see how to move these validations out of controllers into a centralized place.
Model validations
For model validations we need to configure ApiBehaviorOptions in our ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Instance = context.HttpContext.Request.Path,
Status = StatusCodes.Status400BadRequest,
Type = $"https://httpstatuses.com/400",
Detail = ApiConstants.Messages.ModelStateValidation
};
return new BadRequestObjectResult(problemDetails)
{
ContentTypes =
{
ApiConstants.ContentTypes.ProblemJson,
ApiConstants.ContentTypes.ProblemXml
}
};
};
});
}
We have to remark on two things:
- The order in which you register the services matter, you must register AddMvc() before configure ApiBehaviorOptions otherwise you won’t see the correct response.
- Your controllers must be decorated with the [ApiController] attribute:
[Route("api/[controller]")]
[ApiController]
public class BankController : ControllerBase
Handle errors
We have seen how to the validation errors works, but we still need to see how to handle exceptions in our application in order to return a problem details message. Thankfully, Kristian Hellang has already created a NuGet package for this purpose Hellang.Middleware.ProblemDetails.
Install-Package Hellang.Middleware.ProblemDetails -Version 3.0.0
Once we have installed it, we need to configure it. Open your Startup class and add the following code to the ConfigureServices method:
public void ConfigureServices(IServiceCollection services)
{
services
.AddProblemDetails(setup =>
{
setup.IncludeExceptionDetails = _ => Environment.IsDevelopment();
})
...
}
We only include exception details in the problem details messages when we are running in Development mode. It’ll help us to diagnose our HTTP API while we develop.
And finally we need to add problem details to the ASP.NET Core pipeline in Configure method in our Startup class:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app
.UseProblemDetails()
.UseMvc();
}
If you want to test it, you can throw an exception in your action controller:
[HttpPost]
public ActionResult Transfer(TransferInfo model)
{
throw new Exception("Testing problem details");
return Ok();
}
Run your application and execute the action, you should see the following message:
As you can see (In Development mode) we have all the information available related to the exception but if we run our application in non-Development mode all this additional information dissapears:
We only need one more thing: What happens if I want to throw my own exceptions and map to custom problem details objects? No problem, You have a method called Map<> to map exceptions to custom problem details objects. Let me show you:
I’ve created a custom OutOfCreditException (This code must be in a service out of our controllers):
[HttpPost]
public ActionResult Transfer(TransferInfo model)
{
throw new OutOfCreditException(
"You do not have enough credit.",
balance: 30,
cost: 50);
return Ok();
}
I’ve created also my custom problem details object:
internal class OutOfCreditProblemDetails : ProblemDetails
{
public decimal Balance { get; set; }
}
The only thing we need left is to configure ProblemDetails how to map this exception with the problem details object. Open Startup.cs and change the method ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services
.AddProblemDetails(setup =>
{
setup.IncludeExceptionDetails = _ => !Environment.IsDevelopment();
setup.Map<OutOfCreditException>(exception => new OutOfCreditProblemDetails
{
Title = exception.Message,
Detail = exception.Description,
Balance = exception.Balance,
Status = StatusCodes.Status403Forbidden,
Type = exception.Type
});
})
...
}
Run you app and call the controller action and you should see the following message:
Conclusion
In this post I’ve tried to show you a way of specifying errors in HTTP API responses using Problem details and how to avoid to reinvent the wheel in every HTTP API, making easier to our clients handle these messages in a simple and standard way.
Links
Github Hellang.Middleware.ProblemDetails
Annotating the controllers with ApiController
attribute in ASP .NET Core 2.1 or higher will enable the behavioral options for the API’s. These behavioral options include automatic HTTP 400 responses as well.
In this post, we’ll see how we can customize the default error response from the ASP .NET Core Web API.
Default Error Response
If you are creating a new default ASP .NET Core web API project, then you’d see the ValuesController .cs
file in the project. Otherwise, create a Controller and create an action method to a parameter to test the automatic HTTP 400 responses.
If you are creating a custom API for yourself, you must annotate the controller with [ ApiController ]
attribute. Otherwise, the default 400 responses won’t work.
I’ll go with the default ValuesController
for now. We already have the Get
action with id passed in as the parameter.
// GET api/values/5
[HttpGet(“{id}”)]
public ActionResult<string> Get(int id)
{
return “value”;
}
Let’s try to pass in a string for the id parameter for the Get action through Postman.
This is the default error response returned by the API.
Notice that we didn’t have the ModelState checking in our Get
action method. This is because ASP . NET Core did it for us as we have the [ ApiController ]
attribute on top of our controller.
To modify the error response we need to make use of the InvalidModelStateResponseFactory property.
InvalidModelStateResponseFactory
is a delegate, which will invoke the actions annotated with ApiControllerAttribute to convert invalid ModelStateDictionary into an IActionResult .
The default response type for HTTP 400 responses is ValidationProblemDetails class. So, we will create a custom class that inherits ValidationProblemDetails
class and define our custom error messages.
CustomBadRequest Class
Here is our CustomBadRequest
class, which assigns error properties in the construct
public class CustomBadRequest : ValidationProblemDetails
{
public CustomBadRequest(ActionContext context)
{
Title = “Invalid arguments to the API”;
Detail = “The inputs supplied to the API are invalid”;
Status = 400;
ConstructErrorMessages(context);
Type = context.HttpContext.TraceIdentifier;
}
private void ConstructErrorMessages(ActionContext context)
{
foreach (var keyModelStatePair in context.ModelState)
{
var key = keyModelStatePair.Key;
var errors = keyModelStatePair.Value.Errors;
if (errors != null && errors.Count > 0)
{
if (errors.Count == 1)
{
var errorMessage = GetErrorMessage(errors[0]);
Errors.Add(key, new[] { errorMessage });
}
else
{
var errorMessages = new string[errors.Count];
for (var i = 0; i < errors.Count; i++)
{
errorMessages[i] = GetErrorMessage(errors[i]);
}
Errors.Add(key, errorMessages);
}
}
}
}
string GetErrorMessage(ModelError error)
{
return string.IsNullOrEmpty(error.ErrorMessage) ?
“The input was not valid.” :
error.ErrorMessage;
}
}
I’m using ActionContext as a constructor argument as we can have more information about the action. I’ve used the ActionContext as I’m using the TraceIdentifier
from the HttpContext
. The Action context will have route information, HttpContext, ModelState, ActionDescriptor.
You could pass in just the model state in the action context at least for this bad request customization. It’s up to you.
Plugging the CustomBadRequest in the Configuration
We can configure our newly created CustomBadRequest in Configure method in Startup.cs
class in two different ways.
Using ConfigureApiBehaviorOptions Off AddMvc()
ConfigureApiBehaviorOptions
is an extension method on IMvcBuilder
interface. Any method that returns an IMvcBuilder
can call the ConfigureApiBehaviorOptions
method.
public static IMvcBuilder ConfigureApiBehaviorOptions(this IMvcBuilder builder, Action<ApiBehaviorOptions> setupAction);
The AddMvc()
returns an IMvcBuilder
and we will plug our custom bad request here.
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var problems = new CustomBadRequest(context);
return new BadRequestObjectResult(problems);
};
});
Using the Generic Configure Method
This will be convenient, as we don’t chain the configuration here. We’ll be just using the generic configure method here.
services.Configure<ApiBehaviorOptions>(a =>
{
a.InvalidModelStateResponseFactory = context =>
{
var problemDetails = new CustomBadRequest(context);
return new BadRequestObjectResult(problemDetails)
{
ContentTypes = { “application/problem+json”, “application/problem+xml” }
};
};
});
Testing the Custom Bad Request
That’s it for configuring the custom bad request; let’s run the app now.
You can see the customized HTTP 400 error messages we’ve set in our custom bad request class showing up.
Let me know your thoughts in the comments.