I have concerns on the way that we returns errors to client.
Do we return error immediately by throwing HttpResponseException when we get an error:
public void Post(Customer customer)
{
if (string.IsNullOrEmpty(customer.Name))
{
throw new HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest)
}
if (customer.Accounts.Count == 0)
{
throw new HttpResponseException("Customer does not have any account", HttpStatusCode.BadRequest)
}
}
Or we accumulate all errors then send back to client:
public void Post(Customer customer)
{
List<string> errors = new List<string>();
if (string.IsNullOrEmpty(customer.Name))
{
errors.Add("Customer Name cannot be empty");
}
if (customer.Accounts.Count == 0)
{
errors.Add("Customer does not have any account");
}
var responseMessage = new HttpResponseMessage<List<string>>(errors, HttpStatusCode.BadRequest);
throw new HttpResponseException(responseMessage);
}
This is just a sample code, it does not matter either validation errors or server error, I just would like to know the best practice, the pros and cons of each approach.
asked May 24, 2012 at 7:00
3
For me I usually send back an HttpResponseException
and set the status code accordingly depending on the exception thrown and if the exception is fatal or not will determine whether I send back the HttpResponseException
immediately.
At the end of the day it’s an API sending back responses and not views, so I think it’s fine to send back a message with the exception and status code to the consumer. I currently haven’t needed to accumulate errors and send them back as most exceptions are usually due to incorrect parameters or calls etc.
An example in my app is that sometimes the client will ask for data, but there isn’t any data available so I throw a custom NoDataAvailableException
and let it bubble to the Web API app, where then in my custom filter which captures it sending back a relevant message along with the correct status code.
I am not 100% sure on what’s the best practice for this, but this is working for me currently so that’s what I’m doing.
Update:
Since I answered this question a few blog posts have been written on the topic:
https://weblogs.asp.net/fredriknormen/asp-net-web-api-exception-handling
(this one has some new features in the nightly builds)
https://learn.microsoft.com/archive/blogs/youssefm/error-handling-in-asp-net-webapi
Update 2
Update to our error handling process, we have two cases:
-
For general errors like not found, or invalid parameters being passed to an action we return a
HttpResponseException
to stop processing immediately. Additionally for model errors in our actions we will hand the model state dictionary to theRequest.CreateErrorResponse
extension and wrap it in aHttpResponseException
. Adding the model state dictionary results in a list of the model errors sent in the response body. -
For errors that occur in higher layers, server errors, we let the exception bubble to the Web API app, here we have a global exception filter which looks at the exception, logs it with ELMAH and tries to make sense of it setting the correct HTTP status code and a relevant friendly error message as the body again in a
HttpResponseException
. For exceptions that we aren’t expecting the client will receive the default 500 internal server error, but a generic message due to security reasons.
Update 3
Recently, after picking up Web API 2, for sending back general errors we now use the IHttpActionResult interface, specifically the built in classes for in the System.Web.Http.Results
namespace such as NotFound, BadRequest when they fit, if they don’t we extend them, for example a NotFound result with a response message:
public class NotFoundWithMessageResult : IHttpActionResult
{
private string message;
public NotFoundWithMessageResult(string message)
{
this.message = message;
}
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(HttpStatusCode.NotFound);
response.Content = new StringContent(message);
return Task.FromResult(response);
}
}
answered May 24, 2012 at 9:27
gdpgdp
7,90210 gold badges42 silver badges61 bronze badges
9
ASP.NET Web API 2 really simplified it. For example, the following code:
public HttpResponseMessage GetProduct(int id)
{
Product item = repository.Get(id);
if (item == null)
{
var message = string.Format("Product with id = {0} not found", id);
HttpError err = new HttpError(message);
return Request.CreateResponse(HttpStatusCode.NotFound, err);
}
else
{
return Request.CreateResponse(HttpStatusCode.OK, item);
}
}
returns the following content to the browser when the item is not found:
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
Date: Thu, 09 Aug 2012 23:27:18 GMT
Content-Length: 51
{
"Message": "Product with id = 12 not found"
}
Suggestion: Don’t throw HTTP Error 500 unless there is a catastrophic error (for example, WCF Fault Exception). Pick an appropriate HTTP status code that represents the state of your data. (See the apigee link below.)
Links:
- Exception Handling in ASP.NET Web API (asp.net)
and - RESTful API Design: what about errors? (apigee.com)
BrianS
12.8k15 gold badges60 silver badges122 bronze badges
answered Mar 1, 2014 at 0:10
Manish JainManish Jain
9,4895 gold badges39 silver badges44 bronze badges
12
It looks like you’re having more trouble with Validation than errors/exceptions so I’ll say a bit about both.
Validation
Controller actions should generally take Input Models where the validation is declared directly on the model.
public class Customer
{
[Require]
public string Name { get; set; }
}
Then you can use an ActionFilter
that automatically sends validation messages back to the client.
public class ValidationActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var modelState = actionContext.ModelState;
if (!modelState.IsValid) {
actionContext.Response = actionContext.Request
.CreateErrorResponse(HttpStatusCode.BadRequest, modelState);
}
}
}
For more information about this check out http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc
Error handling
It’s best to return a message back to the client that represents the exception that happened (with relevant status code).
Out of the box you have to use Request.CreateErrorResponse(HttpStatusCode, message)
if you want to specify a message. However, this ties the code to the Request
object, which you shouldn’t need to do.
I usually create my own type of «safe» exception that I expect the client would know how to handle and wrap all others with a generic 500 error.
Using an action filter to handle the exceptions would look like this:
public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
var exception = context.Exception as ApiException;
if (exception != null) {
context.Response = context.Request.CreateErrorResponse(exception.StatusCode, exception.Message);
}
}
}
Then you can register it globally.
GlobalConfiguration.Configuration.Filters.Add(new ApiExceptionFilterAttribute());
This is my custom exception type.
using System;
using System.Net;
namespace WebApi
{
public class ApiException : Exception
{
private readonly HttpStatusCode statusCode;
public ApiException (HttpStatusCode statusCode, string message, Exception ex)
: base(message, ex)
{
this.statusCode = statusCode;
}
public ApiException (HttpStatusCode statusCode, string message)
: base(message)
{
this.statusCode = statusCode;
}
public ApiException (HttpStatusCode statusCode)
{
this.statusCode = statusCode;
}
public HttpStatusCode StatusCode
{
get { return this.statusCode; }
}
}
}
An example exception that my API can throw.
public class NotAuthenticatedException : ApiException
{
public NotAuthenticatedException()
: base(HttpStatusCode.Forbidden)
{
}
}
answered Mar 4, 2014 at 5:30
Daniel LittleDaniel Little
16.7k12 gold badges70 silver badges93 bronze badges
3
You can throw a HttpResponseException
HttpResponseMessage response =
this.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "your message");
throw new HttpResponseException(response);
answered Mar 4, 2014 at 5:15
tartakynovtartakynov
2,7383 gold badges25 silver badges22 bronze badges
1
If you are using ASP.NET Web API 2, the easiest way is to use the ApiController Short-Method. This will result in a BadRequestResult.
return BadRequest("message");
answered Mar 22, 2018 at 12:22
1
For Web API 2 my methods consistently return IHttpActionResult so I use…
public IHttpActionResult Save(MyEntity entity)
{
....
if (...errors....)
return ResponseMessage(
Request.CreateResponse(
HttpStatusCode.BadRequest,
validationErrors));
// otherwise success
return Ok(returnData);
}
answered Jan 20, 2016 at 1:53
MickMick
6,3894 gold badges48 silver badges67 bronze badges
1
You can use custom ActionFilter in Web Api to validate model:
public class DRFValidationFilters : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.ModelState.IsValid)
{
actionContext.Response = actionContext.Request
.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
//BadRequest(actionContext.ModelState);
}
}
public override Task OnActionExecutingAsync(HttpActionContext actionContext,
CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
if (!actionContext.ModelState.IsValid)
{
actionContext.Response = actionContext.Request
.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
}
});
}
public class AspirantModel
{
public int AspirantId { get; set; }
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
public string AspirantType { get; set; }
[RegularExpression(@"^(?([0-9]{3}))?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$",
ErrorMessage = "Not a valid Phone number")]
public string MobileNumber { get; set; }
public int StateId { get; set; }
public int CityId { get; set; }
public int CenterId { get; set; }
[HttpPost]
[Route("AspirantCreate")]
[DRFValidationFilters]
public IHttpActionResult Create(AspirantModel aspirant)
{
if (aspirant != null)
{
}
else
{
return Conflict();
}
return Ok();
}
}
}
Register CustomAttribute class in webApiConfig.cs
config.Filters.Add(new DRFValidationFilters());
Metro Smurf
36.7k20 gold badges105 silver badges139 bronze badges
answered Mar 18, 2016 at 9:40
Building up upon Manish Jain
‘s answer (which is meant for Web API 2 which simplifies things):
1) Use validation structures to response as many validation errors as possible. These structures can also be used to response to requests coming from forms.
public class FieldError
{
public String FieldName { get; set; }
public String FieldMessage { get; set; }
}
// a result will be able to inform API client about some general error/information and details information (related to invalid parameter values etc.)
public class ValidationResult<T>
{
public bool IsError { get; set; }
/// <summary>
/// validation message. It is used as a success message if IsError is false, otherwise it is an error message
/// </summary>
public string Message { get; set; } = string.Empty;
public List<FieldError> FieldErrors { get; set; } = new List<FieldError>();
public T Payload { get; set; }
public void AddFieldError(string fieldName, string fieldMessage)
{
if (string.IsNullOrWhiteSpace(fieldName))
throw new ArgumentException("Empty field name");
if (string.IsNullOrWhiteSpace(fieldMessage))
throw new ArgumentException("Empty field message");
// appending error to existing one, if field already contains a message
var existingFieldError = FieldErrors.FirstOrDefault(e => e.FieldName.Equals(fieldName));
if (existingFieldError == null)
FieldErrors.Add(new FieldError {FieldName = fieldName, FieldMessage = fieldMessage});
else
existingFieldError.FieldMessage = $"{existingFieldError.FieldMessage}. {fieldMessage}";
IsError = true;
}
public void AddEmptyFieldError(string fieldName, string contextInfo = null)
{
AddFieldError(fieldName, $"No value provided for field. Context info: {contextInfo}");
}
}
public class ValidationResult : ValidationResult<object>
{
}
2) Service layer will return ValidationResult
s, regardless of operation being successful or not. E.g:
public ValidationResult DoSomeAction(RequestFilters filters)
{
var ret = new ValidationResult();
if (filters.SomeProp1 == null) ret.AddEmptyFieldError(nameof(filters.SomeProp1));
if (filters.SomeOtherProp2 == null) ret.AddFieldError(nameof(filters.SomeOtherProp2 ), $"Failed to parse {filters.SomeOtherProp2} into integer list");
if (filters.MinProp == null) ret.AddEmptyFieldError(nameof(filters.MinProp));
if (filters.MaxProp == null) ret.AddEmptyFieldError(nameof(filters.MaxProp));
// validation affecting multiple input parameters
if (filters.MinProp > filters.MaxProp)
{
ret.AddFieldError(nameof(filters.MinProp, "Min prop cannot be greater than max prop"));
ret.AddFieldError(nameof(filters.MaxProp, "Check"));
}
// also specify a global error message, if we have at least one error
if (ret.IsError)
{
ret.Message = "Failed to perform DoSomeAction";
return ret;
}
ret.Message = "Successfully performed DoSomeAction";
return ret;
}
3) API Controller will construct the response based on service function result
One option is to put virtually all parameters as optional and perform custom validation which return a more meaningful response. Also, I am taking care not to allow any exception to go beyond the service boundary.
[Route("DoSomeAction")]
[HttpPost]
public HttpResponseMessage DoSomeAction(int? someProp1 = null, string someOtherProp2 = null, int? minProp = null, int? maxProp = null)
{
try
{
var filters = new RequestFilters
{
SomeProp1 = someProp1 ,
SomeOtherProp2 = someOtherProp2.TrySplitIntegerList() ,
MinProp = minProp,
MaxProp = maxProp
};
var result = theService.DoSomeAction(filters);
return !result.IsError ? Request.CreateResponse(HttpStatusCode.OK, result) : Request.CreateResponse(HttpStatusCode.BadRequest, result);
}
catch (Exception exc)
{
Logger.Log(LogLevel.Error, exc, "Failed to DoSomeAction");
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, new HttpError("Failed to DoSomeAction - internal error"));
}
}
answered Dec 19, 2016 at 14:32
Use the built in «InternalServerError» method (available in ApiController):
return InternalServerError();
//or...
return InternalServerError(new YourException("your message"));
answered Aug 4, 2017 at 15:03
RustyRusty
1091 silver badge9 bronze badges
Welcome to 2022! Now we have other answers in .NET (since ASP.NET Core 2.1). Look at this article: Using the ProblemDetails Class in ASP.NET Core Web API, where the author explains the following best practices:
- How to implement standard IETF RFC 7807, which defines a «problem detail» as a way to carry machine-readable details of errors in an HTTP response to avoid the need to define new error response formats for HTTP APIs.
- How model validations use the ProblemDetails class to populate a list of validation errors — the direct answer to the question for a general rule, whether to break processing after the first error.
As a teaser, this is how the JSON output looks if we use ProductDetails
and multiple errors:
answered Oct 5, 2022 at 11:01
Just to update on the current state of ASP.NET WebAPI. The interface is now called IActionResult
and implementation hasn’t changed much:
[JsonObject(IsReference = true)]
public class DuplicateEntityException : IActionResult
{
public DuplicateEntityException(object duplicateEntity, object entityId)
{
this.EntityType = duplicateEntity.GetType().Name;
this.EntityId = entityId;
}
/// <summary>
/// Id of the duplicate (new) entity
/// </summary>
public object EntityId { get; set; }
/// <summary>
/// Type of the duplicate (new) entity
/// </summary>
public string EntityType { get; set; }
public Task ExecuteResultAsync(ActionContext context)
{
var message = new StringContent($"{this.EntityType ?? "Entity"} with id {this.EntityId ?? "(no id)"} already exist in the database");
var response = new HttpResponseMessage(HttpStatusCode.Ambiguous) { Content = message };
return Task.FromResult(response);
}
#endregion
}
answered May 10, 2016 at 7:51
Thomas HagströmThomas Hagström
4,1711 gold badge20 silver badges27 bronze badges
2
Try this
[HttpPost]
public async Task<ActionResult<User>> PostUser(int UserTypeId, User user)
{
if (somethingFails)
{
// Return the error message like this.
return new BadRequestObjectResult(new
{
message = "Something is not working here"
});
}
return ok();
}
CinCout
9,41911 gold badges53 silver badges65 bronze badges
answered Dec 17, 2021 at 18:29
ZablonZablon
1171 silver badge4 bronze badges
5
Some of these answers seem to be relics of the past. I’ve found the solution below to be simple and work well. This is in .NET 6 for a Web API derived from ControllerBase
.
Rather than throwing exceptions, you can directly return the various HTTP response codes as objects, along with an exact error message:
using Microsoft.AspNetCore.Mvc;
[ApiController]
public class MyWebApiController : ControllerBase
{
[HttpPost]
public IActionResult Process(Customer customer)
{
if (string.IsNullOrEmpty(customer.Name))
return BadRequest("Customer Name cannot be empty");
if (!Customers.Find(customer))
return NotFound("Customer does not have any account");
// After validating inputs, core logic goes here...
return Ok(customer.ID); // or simply "return Ok()" if not returning data
}
}
See a list of error codes available here.
As for when to return the errors (OP’s question), it depends on the requirement. Returning errors as they happen means you avoid the overhead of additional processing, but then the client has to make repeated calls to get all the errors. Consider the server viewpoint also, as it may cause undesirable program behavior to continue server-side processing when an error has occurred.
answered Sep 1, 2022 at 18:56
Tawab WakilTawab Wakil
1,53117 silver badges31 bronze badges
For those errors where modelstate.isvalid is false, I generally send the error as it is thrown by the code. Its easy to understand for the developer who is consuming my service. I generally send the result using below code.
if(!ModelState.IsValid) {
List<string> errorlist=new List<string>();
foreach (var value in ModelState.Values)
{
foreach(var error in value.Errors)
errorlist.Add( error.Exception.ToString());
//errorlist.Add(value.Errors);
}
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.BadRequest,errorlist);}
This sends the error to the client in below format which is basically a list of errors:
[
"Newtonsoft.Json.JsonReaderException: **Could not convert string to integer: abc. Path 'Country',** line 6, position 16.rn
at Newtonsoft.Json.JsonReader.ReadAsInt32Internal()rn
at Newtonsoft.Json.JsonTextReader.ReadAsInt32()rn
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter, Boolean inArray)rn
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)",
"Newtonsoft.Json.JsonReaderException: **Could not convert string to integer: ab. Path 'State'**, line 7, position 13.rn
at Newtonsoft.Json.JsonReader.ReadAsInt32Internal()rn
at Newtonsoft.Json.JsonTextReader.ReadAsInt32()rn
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter, Boolean inArray)rn
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)"
]
Sylvain
19k23 gold badges92 silver badges144 bronze badges
answered Jan 19, 2016 at 10:41
2
title | author | description | monikerRange | ms.author | ms.custom | ms.date | uid |
---|---|---|---|---|---|---|---|
Handle errors in ASP.NET Core web APIs |
rick-anderson |
Learn about error handling with ASP.NET Core web APIs. |
>= aspnetcore-3.1 |
riande |
mvc |
10/14/2022 |
web-api/handle-errors |
Handle errors in ASP.NET Core web APIs
:::moniker range=»>= aspnetcore-7.0″
This article describes how to handle errors and customize error handling with ASP.NET Core web APIs.
Developer Exception Page
The Developer Exception Page shows detailed stack traces for server errors. It uses xref:Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware to capture synchronous and asynchronous exceptions from the HTTP pipeline and to generate error responses. For example, consider the following controller action, which throws an exception:
:::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Controllers/ErrorsController.cs» id=»snippet_Throw»:::
When the Developer Exception Page detects an unhandled exception, it generates a default plain-text response similar to the following example:
HTTP/1.1 500 Internal Server Error Content-Type: text/plain; charset=utf-8 Server: Kestrel Transfer-Encoding: chunked System.Exception: Sample exception. at HandleErrorsSample.Controllers.ErrorsController.Get() in ... at lambda_method1(Closure , Object , Object[] ) at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync() at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync() ...
If the client requests an HTML-formatted response, the Developer Exception Page generates a response similar to the following example:
HTTP/1.1 500 Internal Server Error Content-Type: text/html; charset=utf-8 Server: Kestrel Transfer-Encoding: chunked <!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta charset="utf-8" /> <title>Internal Server Error</title> <style> body { font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif; font-size: .813em; color: #222; background-color: #fff; } h1 { color: #44525e; margin: 15px 0 15px 0; } ...
To request an HTML-formatted response, set the Accept
HTTP request header to text/html
.
[!WARNING]
Don’t enable the Developer Exception Page unless the app is running in the Development environment. Don’t share detailed exception information publicly when the app runs in production. For more information on configuring environments, see xref:fundamentals/environments.
Exception handler
In non-development environments, use Exception Handling Middleware to produce an error payload:
-
In
Program.cs
, call xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A to add the Exception Handling Middleware::::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Program.cs» id=»snippet_Middleware» highlight=»7″:::
-
Configure a controller action to respond to the
/error
route::::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Controllers/ErrorsController.cs» id=»snippet_HandleError»:::
The preceding HandleError
action sends an RFC 7807-compliant payload to the client.
[!WARNING]
Don’t mark the error handler action method with HTTP method attributes, such asHttpGet
. Explicit verbs prevent some requests from reaching the action method.For web APIs that use Swagger / OpenAPI, mark the error handler action with the [ApiExplorerSettings] attribute and set its xref:Microsoft.AspNetCore.Mvc.ApiExplorerSettingsAttribute.IgnoreApi%2A property to
true
. This attribute configuration excludes the error handler action from the app’s OpenAPI specification:[ApiExplorerSettings(IgnoreApi = true)]Allow anonymous access to the method if unauthenticated users should see the error.
Exception Handling Middleware can also be used in the Development environment to produce a consistent payload format across all environments:
-
In
Program.cs
, register environment-specific Exception Handling Middleware instances::::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Snippets/Program.cs» id=»snippet_ConsistentEnvironments»:::
In the preceding code, the middleware is registered with:
- A route of
/error-development
in the Development environment. - A route of
/error
in non-Development environments.
- A route of
-
Add controller actions for both the Development and non-Development routes:
:::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Controllers/ErrorsController.cs» id=»snippet_ConsistentEnvironments»:::
Use exceptions to modify the response
The contents of the response can be modified from outside of the controller using a custom exception and an action filter:
-
Create a well-known exception type named
HttpResponseException
::::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Snippets/HttpResponseException.cs» id=»snippet_Class»:::
-
Create an action filter named
HttpResponseExceptionFilter
::::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Snippets/HttpResponseExceptionFilter.cs» id=»snippet_Class»:::
The preceding filter specifies an
Order
of the maximum integer value minus 10. ThisOrder
allows other filters to run at the end of the pipeline. -
In
Program.cs
, add the action filter to the filters collection::::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Snippets/Program.cs» id=»snippet_AddHttpResponseExceptionFilter»:::
Validation failure error response
For web API controllers, MVC responds with a xref:Microsoft.AspNetCore.Mvc.ValidationProblemDetails response type when model validation fails. MVC uses the results of xref:Microsoft.AspNetCore.Mvc.ApiBehaviorOptions.InvalidModelStateResponseFactory to construct the error response for a validation failure. The following example replaces the default factory with an implementation that also supports formatting responses as XML, in Program.cs
:
:::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Snippets/Program.cs» id=»snippet_ConfigureInvalidModelStateResponseFactory»:::
Client error response
An error result is defined as a result with an HTTP status code of 400 or higher. For web API controllers, MVC transforms an error result to produce a xref:Microsoft.AspNetCore.Mvc.ProblemDetails.
The automatic creation of a ProblemDetails
for error status codes is enabled by default, but error responses can be configured in one of the following ways:
- Use the problem details service
- Implement ProblemDetailsFactory
- Use ApiBehaviorOptions.ClientErrorMapping
Default problem details response
The following Program.cs
file was generated by the web application templates for API controllers:
:::code language=»csharp» source=»~/../AspNetCore.Docs.Samples/fundamentals/middleware/problem-details-service/Program.cs» id=»snippet_default»:::
Consider the following controller, which returns xref:Microsoft.AspNetCore.Http.HttpResults.BadRequest when the input is invalid:
:::code language=»csharp» source=»~/../AspNetCore.Docs.Samples/fundamentals/middleware/problem-details-service/Controllers/ValuesController.cs» id=»snippet_1″:::
A problem details response is generated with the previous code when any of the following conditions apply:
- The
/api/values2/divide
endpoint is called with a zero denominator. - The
/api/values2/squareroot
endpoint is called with a radicand less than zero.
The default problem details response body has the following type
, title
, and status
values:
{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "Bad Request", "status": 400, "traceId": "00-84c1fd4063c38d9f3900d06e56542d48-85d1d4-00" }
Problem details service
ASP.NET Core supports creating Problem Details for HTTP APIs using the xref:Microsoft.AspNetCore.Http.IProblemDetailsService. For more information, see the Problem details service.
The following code configures the app to generate a problem details response for all HTTP client and server error responses that don’t have a body content yet:
:::code language=»csharp» source=»~/../AspNetCore.Docs.Samples/fundamentals/middleware/problem-details-service/Program.cs» id=»snippet_apishort» highlight=»4,8-9,13″:::
Consider the API controller from the previous section, which returns xref:Microsoft.AspNetCore.Http.HttpResults.BadRequest when the input is invalid:
:::code language=»csharp» source=»~/../AspNetCore.Docs.Samples/fundamentals/middleware/problem-details-service/Controllers/ValuesController.cs» id=»snippet_1″:::
A problem details response is generated with the previous code when any of the following conditions apply:
- An invalid input is supplied.
- The URI has no matching endpoint.
- An unhandled exception occurs.
The automatic creation of a ProblemDetails
for error status codes is disabled when the xref:Microsoft.AspNetCore.Mvc.ApiBehaviorOptions.SuppressMapClientErrors%2A property is set to true
:
:::code language=»csharp» source=»~/../AspNetCore.Docs.Samples/fundamentals/middleware/problem-details-service/Program.cs» id=»snippet_disable» highlight=»4-7″:::
Using the preceding code, when an API controller returns BadRequest
, an HTTP 400 response status is returned with no response body. SuppressMapClientErrors
prevents a ProblemDetails
response from being created, even when calling WriteAsync
for an API Controller endpoint. WriteAsync
is explained later in this article.
The next section shows how to customize the problem details response body, using xref:Microsoft.AspNetCore.Http.ProblemDetailsOptions.CustomizeProblemDetails, to return a more helpful response. For more customization options, see Customizing problem details.
Customize problem details with CustomizeProblemDetails
The following code uses xref:Microsoft.AspNetCore.Http.ProblemDetailsOptions to set xref:Microsoft.AspNetCore.Http.ProblemDetailsOptions.CustomizeProblemDetails:
:::code language=»csharp» source=»~/../AspNetCore.Docs.Samples/fundamentals/middleware/problem-details-service/Program.cs» id=»snippet_api_controller» highlight=»6″:::
The updated API controller:
:::code language=»csharp» source=»~/../AspNetCore.Docs.Samples/fundamentals/middleware/problem-details-service/Controllers/ValuesController.cs» id=»snippet» highlight=»9-17,27-35″:::
The following code contains the MathErrorFeature
and MathErrorType
, which are used with the preceding sample:
:::code language=»csharp» source=»~/../AspNetCore.Docs.Samples/fundamentals/middleware/problem-details-service/MathErrorFeature.cs» :::
A problem details response is generated with the previous code when any of the following conditions apply:
- The
/divide
endpoint is called with a zero denominator. - The
/squareroot
endpoint is called with a radicand less than zero. - The URI has no matching endpoint.
The problem details response body contains the following when either squareroot
endpoint is called with a radicand less than zero:
{ "type": "https://en.wikipedia.org/wiki/Square_root", "title": "Bad Input", "status": 400, "detail": "Negative or complex numbers are not allowed." }
View or download sample code
Implement ProblemDetailsFactory
MVC uses xref:Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory?displayProperty=fullName to produce all instances of xref:Microsoft.AspNetCore.Mvc.ProblemDetails and xref:Microsoft.AspNetCore.Mvc.ValidationProblemDetails. This factory is used for:
- Client error responses
- Validation failure error responses
- xref:Microsoft.AspNetCore.Mvc.ControllerBase.Problem%2A?displayProperty=nameWithType and xref:Microsoft.AspNetCore.Mvc.ControllerBase.ValidationProblem%2A?displayProperty=nameWithType
To customize the problem details response, register a custom implementation of xref:Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory in Program.cs
:
:::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Snippets/Program.cs» id=»snippet_ReplaceProblemDetailsFactory»:::
Use ApiBehaviorOptions.ClientErrorMapping
Use the xref:Microsoft.AspNetCore.Mvc.ApiBehaviorOptions.ClientErrorMapping%2A property to configure the contents of the ProblemDetails
response. For example, the following code in Program.cs
updates the xref:Microsoft.AspNetCore.Mvc.ClientErrorData.Link%2A property for 404 responses:
:::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Snippets/Program.cs» id=»snippet_ClientErrorMapping»:::
Additional resources
- How to Use ModelState Validation in ASP.NET Core Web API
- View or download sample code
- Hellang.Middleware.ProblemDetails
:::moniker-end
:::moniker range=»= aspnetcore-6.0″
This article describes how to handle errors and customize error handling with ASP.NET Core web APIs.
Developer Exception Page
The Developer Exception Page shows detailed stack traces for server errors. It uses xref:Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware to capture synchronous and asynchronous exceptions from the HTTP pipeline and to generate error responses. For example, consider the following controller action, which throws an exception:
:::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Controllers/ErrorsController.cs» id=»snippet_Throw»:::
When the Developer Exception Page detects an unhandled exception, it generates a default plain-text response similar to the following example:
HTTP/1.1 500 Internal Server Error Content-Type: text/plain; charset=utf-8 Server: Kestrel Transfer-Encoding: chunked System.Exception: Sample exception. at HandleErrorsSample.Controllers.ErrorsController.Get() in ... at lambda_method1(Closure , Object , Object[] ) at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync() at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync() ...
If the client requests an HTML-formatted response, the Developer Exception Page generates a response similar to the following example:
HTTP/1.1 500 Internal Server Error Content-Type: text/html; charset=utf-8 Server: Kestrel Transfer-Encoding: chunked <!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta charset="utf-8" /> <title>Internal Server Error</title> <style> body { font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif; font-size: .813em; color: #222; background-color: #fff; } h1 { color: #44525e; margin: 15px 0 15px 0; } ...
To request an HTML-formatted response, set the Accept
HTTP request header to text/html
.
[!WARNING]
Don’t enable the Developer Exception Page unless the app is running in the Development environment. Don’t share detailed exception information publicly when the app runs in production. For more information on configuring environments, see xref:fundamentals/environments.
Exception handler
In non-development environments, use Exception Handling Middleware to produce an error payload:
-
In
Program.cs
, call xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A to add the Exception Handling Middleware::::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Program.cs» id=»snippet_Middleware» highlight=»7″:::
-
Configure a controller action to respond to the
/error
route::::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Controllers/ErrorsController.cs» id=»snippet_HandleError»:::
The preceding HandleError
action sends an RFC 7807-compliant payload to the client.
[!WARNING]
Don’t mark the error handler action method with HTTP method attributes, such asHttpGet
. Explicit verbs prevent some requests from reaching the action method.For web APIs that use Swagger / OpenAPI, mark the error handler action with the [ApiExplorerSettings] attribute and set its xref:Microsoft.AspNetCore.Mvc.ApiExplorerSettingsAttribute.IgnoreApi%2A property to
true
. This attribute configuration excludes the error handler action from the app’s OpenAPI specification:[ApiExplorerSettings(IgnoreApi = true)]Allow anonymous access to the method if unauthenticated users should see the error.
Exception Handling Middleware can also be used in the Development environment to produce a consistent payload format across all environments:
-
In
Program.cs
, register environment-specific Exception Handling Middleware instances::::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Snippets/Program.cs» id=»snippet_ConsistentEnvironments»:::
In the preceding code, the middleware is registered with:
- A route of
/error-development
in the Development environment. - A route of
/error
in non-Development environments.
- A route of
-
Add controller actions for both the Development and non-Development routes:
:::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Controllers/ErrorsController.cs» id=»snippet_ConsistentEnvironments»:::
Use exceptions to modify the response
The contents of the response can be modified from outside of the controller using a custom exception and an action filter:
-
Create a well-known exception type named
HttpResponseException
::::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Snippets/HttpResponseException.cs» id=»snippet_Class»:::
-
Create an action filter named
HttpResponseExceptionFilter
::::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Snippets/HttpResponseExceptionFilter.cs» id=»snippet_Class»:::
The preceding filter specifies an
Order
of the maximum integer value minus 10. ThisOrder
allows other filters to run at the end of the pipeline. -
In
Program.cs
, add the action filter to the filters collection::::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Snippets/Program.cs» id=»snippet_AddHttpResponseExceptionFilter»:::
Validation failure error response
For web API controllers, MVC responds with a xref:Microsoft.AspNetCore.Mvc.ValidationProblemDetails response type when model validation fails. MVC uses the results of xref:Microsoft.AspNetCore.Mvc.ApiBehaviorOptions.InvalidModelStateResponseFactory to construct the error response for a validation failure. The following example replaces the default factory with an implementation that also supports formatting responses as XML, in Program.cs
:
:::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Snippets/Program.cs» id=»snippet_ConfigureInvalidModelStateResponseFactory»:::
Client error response
An error result is defined as a result with an HTTP status code of 400 or higher. For web API controllers, MVC transforms an error result to produce a xref:Microsoft.AspNetCore.Mvc.ProblemDetails.
The error response can be configured in one of the following ways:
- Implement ProblemDetailsFactory
- Use ApiBehaviorOptions.ClientErrorMapping
Implement ProblemDetailsFactory
MVC uses xref:Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory?displayProperty=fullName to produce all instances of xref:Microsoft.AspNetCore.Mvc.ProblemDetails and xref:Microsoft.AspNetCore.Mvc.ValidationProblemDetails. This factory is used for:
- Client error responses
- Validation failure error responses
- xref:Microsoft.AspNetCore.Mvc.ControllerBase.Problem%2A?displayProperty=nameWithType and xref:Microsoft.AspNetCore.Mvc.ControllerBase.ValidationProblem%2A?displayProperty=nameWithType
To customize the problem details response, register a custom implementation of xref:Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory in Program.cs
:
:::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Snippets/Program.cs» id=»snippet_ReplaceProblemDetailsFactory»:::
Use ApiBehaviorOptions.ClientErrorMapping
Use the xref:Microsoft.AspNetCore.Mvc.ApiBehaviorOptions.ClientErrorMapping%2A property to configure the contents of the ProblemDetails
response. For example, the following code in Program.cs
updates the xref:Microsoft.AspNetCore.Mvc.ClientErrorData.Link%2A property for 404 responses:
:::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Snippets/Program.cs» id=»snippet_ClientErrorMapping»:::
Custom Middleware to handle exceptions
The defaults in the exception handling middleware work well for most apps. For apps that require specialized exception handling, consider customizing the exception handling middleware.
Produce a ProblemDetails payload for exceptions
ASP.NET Core doesn’t produce a standardized error payload when an unhandled exception occurs. For scenarios where it’s desirable to return a standardized ProblemDetails response to the client, the ProblemDetails middleware can be used to map exceptions and 404 responses to a ProblemDetails payload. The exception handling middleware can also be used to return a xref:Microsoft.AspNetCore.Mvc.ProblemDetails payload for unhandled exceptions.
Additional resources
- How to Use ModelState Validation in ASP.NET Core Web API
- View or download sample code (How to download)
:::moniker-end
:::moniker range=»< aspnetcore-6.0″
This article describes how to handle and customize error handling with ASP.NET Core web APIs.
View or download sample code (How to download)
Developer Exception Page
The Developer Exception Page is a useful tool to get detailed stack traces for server errors. It uses xref:Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware to capture synchronous and asynchronous exceptions from the HTTP pipeline and to generate error responses. To illustrate, consider the following controller action:
:::code language=»csharp» source=»handle-errors/samples/3.x/Controllers/WeatherForecastController.cs» id=»snippet_GetByCity»:::
Run the following curl
command to test the preceding action:
curl -i https://localhost:5001/weatherforecast/chicago
The Developer Exception Page displays a plain-text response if the client doesn’t request HTML-formatted output. The following output appears:
HTTP/1.1 500 Internal Server Error Transfer-Encoding: chunked Content-Type: text/plain Server: Microsoft-IIS/10.0 X-Powered-By: ASP.NET Date: Fri, 27 Sep 2019 16:13:16 GMT System.ArgumentException: We don't offer a weather forecast for chicago. (Parameter 'city') at WebApiSample.Controllers.WeatherForecastController.Get(String city) in C:working_folderaspnetAspNetCore.Docsaspnetcoreweb-apihandle-errorssamples3.xControllersWeatherForecastController.cs:line 34 at lambda_method(Closure , Object , Object[] ) at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters) at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync() --- End of stack trace from previous location where exception was thrown --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker) at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context) HEADERS ======= Accept: */* Host: localhost:44312 User-Agent: curl/7.55.1
To display an HTML-formatted response instead, set the Accept
HTTP request header to the text/html
media type. For example:
curl -i -H "Accept: text/html" https://localhost:5001/weatherforecast/chicago
Consider the following excerpt from the HTTP response:
HTTP/1.1 500 Internal Server Error Transfer-Encoding: chunked Content-Type: text/html; charset=utf-8 Server: Microsoft-IIS/10.0 X-Powered-By: ASP.NET Date: Fri, 27 Sep 2019 16:55:37 GMT <!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta charset="utf-8" /> <title>Internal Server Error</title> <style> body { font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif; font-size: .813em; color: #222; background-color: #fff; }
The HTML-formatted response becomes useful when testing via tools like Postman. The following screen capture shows both the plain-text and the HTML-formatted responses in Postman:
:::image source=»handle-errors/_static/developer-exception-page-postman.gif» alt-text=»Test the Developer Exception Page in Postman.»:::
[!WARNING]
Enable the Developer Exception Page only when the app is running in the Development environment. Don’t share detailed exception information publicly when the app runs in production. For more information on configuring environments, see xref:fundamentals/environments.Don’t mark the error handler action method with HTTP method attributes, such as
HttpGet
. Explicit verbs prevent some requests from reaching the action method. Allow anonymous access to the method if unauthenticated users should see the error.
Exception handler
In non-development environments, Exception Handling Middleware can be used to produce an error payload:
-
In
Startup.Configure
, invoke xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A to use the middleware::::code language=»csharp» source=»handle-errors/samples/3.x/Startup.cs» id=»snippet_UseExceptionHandler» highlight=»9″:::
-
Configure a controller action to respond to the
/error
route::::code language=»csharp» source=»handle-errors/samples/3.x/Controllers/ErrorController.cs» id=»snippet_ErrorController»:::
The preceding Error
action sends an RFC 7807-compliant payload to the client.
Exception Handling Middleware can also provide more detailed content-negotiated output in the local development environment. Use the following steps to produce a consistent payload format across development and production environments:
-
In
Startup.Configure
, register environment-specific Exception Handling Middleware instances:public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseExceptionHandler("/error-local-development"); } else { app.UseExceptionHandler("/error"); } }
In the preceding code, the middleware is registered with:
- A route of
/error-local-development
in the Development environment. - A route of
/error
in environments that aren’t Development.
- A route of
-
Apply attribute routing to controller actions:
:::code language=»csharp» source=»handle-errors/samples/3.x/Controllers/ErrorController.cs» id=»snippet_ErrorControllerEnvironmentSpecific»:::
The preceding code calls ControllerBase.Problem to create a xref:Microsoft.AspNetCore.Mvc.ProblemDetails response.
Use exceptions to modify the response
The contents of the response can be modified from outside of the controller. In ASP.NET 4.x Web API, one way to do this was using the xref:System.Web.Http.HttpResponseException type. ASP.NET Core doesn’t include an equivalent type. Support for HttpResponseException
can be added with the following steps:
-
Create a well-known exception type named
HttpResponseException
::::code language=»csharp» source=»handle-errors/samples/3.x/Exceptions/HttpResponseException.cs» id=»snippet_HttpResponseException»:::
-
Create an action filter named
HttpResponseExceptionFilter
::::code language=»csharp» source=»handle-errors/samples/3.x/Filters/HttpResponseExceptionFilter.cs» id=»snippet_HttpResponseExceptionFilter»:::
The preceding filter specifies an
Order
of the maximum integer value minus 10. ThisOrder
allows other filters to run at the end of the pipeline. -
In
Startup.ConfigureServices
, add the action filter to the filters collection::::code language=»csharp» source=»handle-errors/samples/3.x/Startup.cs» id=»snippet_AddExceptionFilter»:::
Validation failure error response
For web API controllers, MVC responds with a xref:Microsoft.AspNetCore.Mvc.ValidationProblemDetails response type when model validation fails. MVC uses the results of xref:Microsoft.AspNetCore.Mvc.ApiBehaviorOptions.InvalidModelStateResponseFactory to construct the error response for a validation failure. The following example uses the factory to change the default response type to xref:Microsoft.AspNetCore.Mvc.SerializableError in Startup.ConfigureServices
:
:::code language=»csharp» source=»handle-errors/samples/3.x/Startup.cs» id=»snippet_DisableProblemDetailsInvalidModelStateResponseFactory» highlight=»4-13″:::
Client error response
An error result is defined as a result with an HTTP status code of 400 or higher. For web API controllers, MVC transforms an error result to a result with xref:Microsoft.AspNetCore.Mvc.ProblemDetails.
The error response can be configured in one of the following ways:
- Implement ProblemDetailsFactory
- Use ApiBehaviorOptions.ClientErrorMapping
Implement ProblemDetailsFactory
MVC uses xref:Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory?displayProperty=fullName to produce all instances of xref:Microsoft.AspNetCore.Mvc.ProblemDetails and xref:Microsoft.AspNetCore.Mvc.ValidationProblemDetails. This factory is used for:
- Client error responses
- Validation failure error responses
- xref:Microsoft.AspNetCore.Mvc.ControllerBase.Problem%2A?displayProperty=nameWithType and xref:Microsoft.AspNetCore.Mvc.ControllerBase.ValidationProblem%2A?displayProperty=nameWithType >
To customize the problem details response, register a custom implementation of xref:Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory in Startup.ConfigureServices
:
public void ConfigureServices(IServiceCollection serviceCollection) { services.AddControllers(); services.AddTransient<ProblemDetailsFactory, CustomProblemDetailsFactory>(); }
Use ApiBehaviorOptions.ClientErrorMapping
Use the xref:Microsoft.AspNetCore.Mvc.ApiBehaviorOptions.ClientErrorMapping%2A property to configure the contents of the ProblemDetails
response. For example, the following code in Startup.ConfigureServices
updates the type
property for 404 responses:
:::code language=»csharp» source=»index/samples/3.x/Startup.cs» id=»snippet_ConfigureApiBehaviorOptions» highlight=»8-9″:::
Custom Middleware to handle exceptions
The defaults in the exception handling middleware work well for most apps. For apps that require specialized exception handling, consider customizing the exception handling middleware.
Producing a ProblemDetails payload for exceptions
ASP.NET Core doesn’t produce a standardized error payload when an unhandled exception occurs. For scenarios where it’s desirable to return a standardized ProblemDetails response to the client, the ProblemDetails middleware can be used to map exceptions and 404 responses to a ProblemDetails payload. The exception handling middleware can also be used to return a xref:Microsoft.AspNetCore.Mvc.ProblemDetails payload for unhandled exceptions.
:::moniker-end
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