Web api response error

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 (s...

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.

Guido Leenders's user avatar

asked May 24, 2012 at 7:00

cuongle's user avatar

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:

  1. 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 the Request.CreateErrorResponse extension and wrap it in a HttpResponseException. Adding the model state dictionary results in a list of the model errors sent in the response body.

  2. 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);
    }
}

Callum Watkins's user avatar

answered May 24, 2012 at 9:27

gdp's user avatar

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's user avatar

BrianS

12.8k15 gold badges60 silver badges122 bronze badges

answered Mar 1, 2014 at 0:10

Manish Jain's user avatar

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)
    {
    }
}

Christian Casutt's user avatar

answered Mar 4, 2014 at 5:30

Daniel Little's user avatar

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

tartakynov's user avatar

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

Fabian von Ellerts's user avatar

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

Mick's user avatar

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's user avatar

Metro Smurf

36.7k20 gold badges105 silver badges139 bronze badges

answered Mar 18, 2016 at 9:40

LokeshChikkala's user avatar

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 ValidationResults, 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

Alexei - check Codidact's user avatar

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

Rusty's user avatar

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:

  1. 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.
  2. 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:

enter image description here

answered Oct 5, 2022 at 11:01

Gerard Jaryczewski's user avatar

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öm's user avatar

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's user avatar

CinCout

9,41911 gold badges53 silver badges65 bronze badges

answered Dec 17, 2021 at 18:29

Zablon's user avatar

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 Wakil's user avatar

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's user avatar

Sylvain

19k23 gold badges92 silver badges144 bronze badges

answered Jan 19, 2016 at 10:41

Ashish Sahu's user avatar

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:

  1. 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″:::

  2. 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 as HttpGet. 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:

  1. 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.
  2. 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:

  1. Create a well-known exception type named HttpResponseException:

    :::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Snippets/HttpResponseException.cs» id=»snippet_Class»:::

  2. 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. This Order allows other filters to run at the end of the pipeline.

  3. 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:

  1. Use the problem details service
  2. Implement ProblemDetailsFactory
  3. 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:

  1. 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″:::

  2. 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 as HttpGet. 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:

  1. 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.
  2. 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:

  1. Create a well-known exception type named HttpResponseException:

    :::code language=»csharp» source=»handle-errors/samples/6.x/HandleErrorsSample/Snippets/HttpResponseException.cs» id=»snippet_Class»:::

  2. 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. This Order allows other filters to run at the end of the pipeline.

  3. 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:

  1. Implement ProblemDetailsFactory
  2. 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:

  1. 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″:::

  2. 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:

  1. 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.
  2. 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:

  1. Create a well-known exception type named HttpResponseException:

    :::code language=»csharp» source=»handle-errors/samples/3.x/Exceptions/HttpResponseException.cs» id=»snippet_HttpResponseException»:::

  2. 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. This Order allows other filters to run at the end of the pipeline.

  3. 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:

  1. Implement ProblemDetailsFactory
  2. 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

Comments

Понравилась статья? Поделить с друзьями:
  • Web api error not found
  • Web api 500 internal server error
  • Web compiler found an error in compilerconfig json
  • Wearable lanterns ошибка профиль чтение запись поврежден
  • Web client error