Обработка ошибок
Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core 7
Последнее обновление: 06.11.2019
Ошибки в приложении можно условно разделить на два типа: исключения, которые возникают в процессе выполнения кода (например, деление на 0), и
стандартные ошибки протокола HTTP (например, ошибка 404).
Обычные исключения могут быть полезны для разработчика в процессе создания приложения, но простые пользователи не должны будут их видеть.
UseDeveloperExceptionPage
Если мы создаем проект ASP.NET Core, например, по типу Empty (да и в других типах проектов), то в классе Startup мы можем найти в начале метода Configure()
следующие строки:
if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); }
Если приложение находится в состоянии разработки, то с помощью middleware app.UseDeveloperExceptionPage()
приложение перехватывает исключения и
выводит информацию о них разработчику.
Например, изменим класс Startup следующим образом:
public class Startup { public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.Run(async (context) => { int x = 0; int y = 8 / x; await context.Response.WriteAsync($"Result = {y}"); }); } }
В middleware app.Run симулируется генерация исключения при делении ноль. И если мы запустим проект, то в браузере мы увидим
информацию об исключении:
Этой информации достаточно, чтобы определить где именно в коде произошло исключение.
Теперь посмотрим, как все это будет выглядеть для простого пользователя. Для этого изменим метод Configure:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { env.EnvironmentName = "Production"; if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.Run(async (context) => { int x = 0; int y = 8 / x; await context.Response.WriteAsync($"Result = {y}"); }); }
Выражение env.EnvironmentName = "Production";
устанавливает режим развертывания вместо режима разработки. В этом случае выражение if (env.IsDevelopment())
будет возвращать false, и мы увидим в браузере что-то наподобие «HTTP ERROR 500»
UseExceptionHandler
Это не самая лучшая ситуация, и нередко все-таки возникает необходимость дать пользователям некоторую информацию о том, что же все-таки произошло. Либо потребуется как-то обработать данную ситуацию.
Для этих целей можно использовать еще один встроенный middleware в виде метода UseExceptionHandler(). Он перенаправляет
при возникновении исключения на некоторый адрес и позволяет обработать исключение. Например, изменим метод Configure следующим образом:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { env.EnvironmentName = "Production"; if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/error"); } app.Map("/error", ap => ap.Run(async context => { await context.Response.WriteAsync("DivideByZeroException occured!"); })); app.Run(async (context) => { int x = 0; int y = 8 / x; await context.Response.WriteAsync($"Result = {y}"); }); }
Метод app.UseExceptionHandler("/error");
перенаправляет при возникновении ошибки на адрес «/error».
Для обработки пути по определенному адресу здесь использовался метод app.Map()
. В итоге при возникновении исключения будет срабатывать делегат
из метода app.Map.
Следует учитывать, что оба middleware — app.UseDeveloperExceptionPage()
и app.UseExceptionHandler()
следует помещать ближе к началу конвейера middleware.
Обработка ошибок HTTP
В отличие от исключений стандартный функционал проекта ASP.NET Core почти никак не обрабатывает ошибки HTTP, например, в случае если ресурс не найден.
При обращении к несуществующему ресурсу мы увидим в браузере пустую страницу, и только через консоль веб-браузера мы сможем увидеть статусный код.
Но с помощью компонента StatusCodePagesMiddleware можно добавить в проект отправку информации о статусном коде.
Для этого добавим в метод Configure()
класса Startup вызов app.UseStatusCodePages()
:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // обработка ошибок HTTP app.UseStatusCodePages(); app.Map("/hello", ap => ap.Run(async (context) => { await context.Response.WriteAsync($"Hello ASP.NET Core"); })); }
Здесь мы можем обращаться только по адресу «/hello». При обращении ко всем остальным адресам браузер отобразит базовую информацию об ошибке:
Данный метод позволяет настроить отправляемое пользователю сообщение. В частности, мы можем изменить вызов метода так:
app.UseStatusCodePages("text/plain", "Error. Status code : {0}");
В качестве первого параметра указывается MIME-тип ответа, а в качестве второго — собственно то сообщение, которое увидит пользователь. В сообщение мы можем
передать код ошибки через плейсхолдер «{0}».
Вместо метода app.UseStatusCodePages()
мы также можем использовать еще пару других, которые также обрабатываю ошибки HTTP.
С помощью метода app.UseStatusCodePagesWithRedirects()
можно выполнить переадресацию на определенный метод, который непосредственно обработает статусный код:
app.UseStatusCodePagesWithRedirects("/error?code={0}");
Здесь будет идти перенаправление по адресу «/error?code={0}». В качестве параметра через плейсхолдер «{0}» будет передаваться статусный код
ошибки.
Но теперь при обращении к несуществующему ресурсу клиент получит статусный код 302 / Found. То есть формально несуществующий ресурс будет существовать, просто статусный код 302
будет указывать, что ресурс перемещен на другое место — по пути «/error/404».
Подобное поведение может быть неудобно, особенно с точки зрения поисковой индексации, и в этом случае мы можем применить другой метод
app.UseStatusCodePagesWithReExecute():
app.UseStatusCodePagesWithReExecute("/error", "?code={0}");
Первый параметр метода указывает на путь перенаправления, а второй задает параметры строки запроса, которые будут передаваться при перенаправлении.
Вместо плейсхолдера {0} опять же будет передаваться статусный код ошибки. Формально мы получим тот же ответ, так как так же будет идти перенаправление на путь «/error?code=404». Но теперь браузер получит оригинальный статусный код 404.
Пример использования:
public void Configure(IApplicationBuilder app) { // обработка ошибок HTTP app.UseStatusCodePagesWithReExecute("/error", "?code={0}"); app.Map("/error", ap => ap.Run(async context => { await context.Response.WriteAsync($"Err: {context.Request.Query["code"]}"); })); app.Map("/hello", ap => ap.Run(async (context) => { await context.Response.WriteAsync($"Hello ASP.NET Core"); })); }
Настройка обработки ошибок в web.config
Еще один способ обработки кодов ошибок представляет собой определение и настройка в файле конфигурации web.config элемента
httpErrors. Этот способ в принципе использовался и в других версиях ASP.NET.
В ASP.NET Core он также доступен, однако имеет очень ограниченное действие. В частности, мы его можем использовать только при развертывании на IIS, а также не можем использовать ряд настроек.
Итак, добавим в корень проекта новый элемент Web Configurarion File, который естественно назовем web.config:
Изменим его следующим образом:
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <httpErrors errorMode="Custom" existingResponse="Replace"> <remove statusCode="404"/> <remove statusCode="403"/> <error statusCode="404" path="404.html" responseMode="File"/> <error statusCode="403" path="403.html" responseMode="File"/> </httpErrors> <handlers> <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/> </handlers> <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".logsstdout" forwardWindowsAuthToken="false"/> </system.webServer> </configuration>
Также для обработки ошибок добавим в корень проекта новый файл 404.html:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Ошибка 404</title> </head> <body> <h1>Ошибка 404</h1> <h2>Ресурс не найден!</h2> </body> </html>
По аналогии можно добавить файл 403.html для ошибки 403.
Итак, элемент httpErrors имеет ряд настроек. Для тестирования настроек локально, необходимо установить атрибут errorMode="Custom"
.
Если тестирование необязательно, и приложение уже развернуто для использования, то можно установить значение errorMode="DetailedLocalOnly"
.
Значение existingResponse="Replace"
позволит отобразить ошибку по оригинальному запрошенному пути без переадресации.
Внутри элемента httpErrors с помощью отдельных элементов error устанавливается обработка ошибок. Атрибут statusCode
задает статусный код, атрибут path
— адрес url, который будет вызываться, а атрибут responseMode
указывает, как будет обрабатываться ответ вызванному url.
Атрибут responseMode
имеет значение File
, что позволяет рассматривать адрес url из атрибута path как статическую страницу и использовать ее в качестве ответа
Настройки элемента httpErrors
могут наследоваться с других уровней, например, от файла конфигурации machine.config
. И чтобы удалить
все унаследованные настройки, применяется элемент <clear />
. Чтобы удалить настройки для отдельных ошибок, применяется элемент
<remove />
.
Для тестирования используем следующий класс Startup:
public class Startup { public void Configure(IApplicationBuilder app) { app.Map("/hello", ap => ap.Run(async (context) => { await context.Response.WriteAsync($"Hello ASP.NET Core"); })); } }
И после обращения к несуществующему ресурсу в приложении отобразится содержимое из файла 404.html.
title | author | description | monikerRange | ms.author | ms.custom | ms.date | uid |
---|---|---|---|---|---|---|---|
Handle errors in ASP.NET Core web APIs |
tdykstra |
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
Quick and Easy Exception Handling
Simply add this middleware before ASP.NET routing into your middleware registrations.
app.UseExceptionHandler(c => c.Run(async context =>
{
var exception = context.Features
.Get<IExceptionHandlerPathFeature>()
.Error;
var response = new { error = exception.Message };
await context.Response.WriteAsJsonAsync(response);
}));
app.UseMvc(); // or .UseRouting() or .UseEndpoints()
Done!
Enable Dependency Injection for logging and other purposes
Step 1. In your startup, register your exception handling route:
// It should be one of your very first registrations
app.UseExceptionHandler("/error"); // Add this
app.UseEndpoints(endpoints => endpoints.MapControllers());
Step 2. Create controller that will handle all exceptions and produce error response:
[AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)]
public class ErrorsController : ControllerBase
{
[Route("error")]
public MyErrorResponse Error()
{
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
var exception = context.Error; // Your exception
var code = 500; // Internal Server Error by default
if (exception is MyNotFoundException) code = 404; // Not Found
else if (exception is MyUnauthException) code = 401; // Unauthorized
else if (exception is MyException) code = 400; // Bad Request
Response.StatusCode = code; // You can use HttpStatusCode enum instead
return new MyErrorResponse(exception); // Your error model
}
}
A few important notes and observations:
- You can inject your dependencies into the Controller’s constructor.
[ApiExplorerSettings(IgnoreApi = true)]
is needed. Otherwise, it may break your Swashbuckle swagger- Again,
app.UseExceptionHandler("/error");
has to be one of the very top registrations in your StartupConfigure(...)
method. It’s probably safe to place it at the top of the method. - The path in
app.UseExceptionHandler("/error")
and in controller[Route("error")]
should be the same, to allow the controller handle exceptions redirected from exception handler middleware.
Here is the link to official Microsoft documentation.
Response model ideas.
Implement your own response model and exceptions.
This example is just a good starting point. Every service would need to handle exceptions in its own way. With the described approach you have full flexibility and control over handling exceptions and returning the right response from your service.
An example of error response model (just to give you some ideas):
public class MyErrorResponse
{
public string Type { get; set; }
public string Message { get; set; }
public string StackTrace { get; set; }
public MyErrorResponse(Exception ex)
{
Type = ex.GetType().Name;
Message = ex.Message;
StackTrace = ex.ToString();
}
}
For simpler services, you might want to implement http status code exception that would look like this:
public class HttpStatusException : Exception
{
public HttpStatusCode Status { get; private set; }
public HttpStatusException(HttpStatusCode status, string msg) : base(msg)
{
Status = status;
}
}
This can be thrown from anywhere this way:
throw new HttpStatusCodeException(HttpStatusCode.NotFound, "User not found");
Then your handling code could be simplified to just this:
if (exception is HttpStatusException httpException)
{
code = (int) httpException.Status;
}
HttpContext.Features.Get<IExceptionHandlerFeature>()
WAT?
ASP.NET Core developers embraced the concept of middlewares where different aspects of functionality such as Auth, MVC, Swagger etc. are separated and executed sequentially in the request processing pipeline. Each middleware has access to request context and can write into the response if needed. Taking exception handling out of MVC makes sense if it’s important to handle errors from non-MVC middlewares the same way as MVC exceptions, which I find is very common in real world apps. So because built-in exception handling middleware is not a part of MVC, MVC itself knows nothing about it and vice versa, exception handling middleware doesn’t really know where the exception is coming from, besides of course it knows that it happened somewhere down the pipe of request execution. But both may needed to be «connected» with one another. So when exception is not caught anywhere, exception handling middleware catches it and re-runs the pipeline for a route, registered in it. This is how you can «pass» exception handling back to MVC with consistent content negotiation or some other middleware if you wish. The exception itself is extracted from the common middleware context. Looks funny but gets the job done :).
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
In this article, we will learn about Global Exception Handling in ASP.NET Core applications. Exceptions are something inevitable in any application however well the codebase is. This can usually occur due to external factors as well, like network issues and so on. If these exceptions are not handled well within the application, it may even lead the entire application to terminations and data loss.
The source code for the implementation can be found here.
Getting started with Exception Handling in ASP.NET Core
For this demonstration, We will be working on a new ASP.NET Core Web API Project. I will be using Visual Studio 2019 as my default IDE.
The try-catch block is our go-to approach when it comes to quick exception handling. Let’s see a code snippet that demonstrates the same.
[HttpGet] public IActionResult Get() { try { var data = GetData(); //Assume you get some data here which is also likely to throw an exception in certain cases. return Ok(data); } catch (Exception ex) { _logger.LogError(ex.Message); return StatusCode(500); } }
Here is a basic implementation that we are all used to, yeah? Assume, the method GetData() is a service call that is also prone to exceptions due to certain external factors. The thrown exception is caught by the catch block whose responsibility is to log the error to the console and returns a status code of 500 Internal Server Error in this scenario.
To learn more about logging in ASP.NET Core Applications, I recommend you to go through the following articles that demonstrate Logging with probably the best 2 Logging Frameworks for ASP.NET Core – Serilog & NLog
Let’s say that there was an exception during the execution of the Get() method. The below code is the exception that gets triggered.
throw new Exception("An error occurred...");
Here is what you would be seeing on Swagger.
The Console may get you a bit more details on the exception, like the line number and other trace logs.
Although this is a simple way for handling exceptions, this can also increase the lines of code of our application. Yes, you could have this approach for very simple and small applications. Imagine having to write the try-catch block in each and every controller’s action and other service methods. Pretty repetitive and not feasible, yeah?
It would be ideal if there was a way to handle all the exceptions centrally in one location, right? In the next sections, we will see 2 such approaches that can drastically improve our exception handling mechanism by isolating all the handling logics to a single area. This not only gives a better codebase but a more controlled application with even lesser exception handling concerns.
Default Exception Handling Middleware in ASP.NET Core
To make things easier, UseExceptionHandler Middleware comes out of the box with ASP.NET Core applications. This when configured in the Configure method of the startup class adds a middleware to the pipeline of the application that will catch any exceptions in and out of the application. That’s how Middlewares and pipelines work, yeah?
Let’s see how UseExceptionHandler is implemented. Open up the Configure method in the Startup class of your ASP.NET Core application and configure the following.
app.UseExceptionHandler( options => { options.Run(async context => { context.Response.StatusCode = (int)HttpStatusCode.BadRequest; context.Response.ContentType = "text/html"; var exceptionObject = context.Features.Get<IExceptionHandlerFeature>(); if (null != exceptionObject) { var errorMessage = $"{exceptionObject.Error.Message}"; await context.Response.WriteAsync(errorMessage).ConfigureAwait(false); }}); } );
This is a very basic setup & usage of UseExceptionHandler Middleware. So, whenever there is an exception that is detected within the Pipeline of the application, the control falls back to this middleware, which in return will send a custom response to the request sender.
In this case, a status code of 400 Bad Request is sent along with the Message content of the original exception which in our scenario is ‘An error occurred…’. Pretty straight-forward, yeah? Here is how the exception is displayed on Swagger.
Now, whenever there is an exception thrown in any part of the application, this middleware catches it and throws the required exception back to the consumer. Much cleaned-up code, yeah? But there are still more ways to make this better, by miles.
Custom Middleware – Global Exception Handling In ASP.NET Core
In this section let’s create a Custom Global Exception Handling Middleware that gives even more control to the developer and makes the entire process much better.
Custom Global Exception Handling Middleware – Firstly, what is it? It’s a piece of code that can be configured as a middleware in the ASP.NET Core pipeline which contains our custom error handling logics. There are a variety of exceptions that can be caught by this pipeline.
We will also be creating Custom Exception classes that can essentially make your application throw more sensible exceptions that can be easily understood.
But before that, let’s build a Response class that I recommend to be a part of every project you build, at least the concept. So, the idea is to make your ASP.NET Core API send uniform responses no matter what kind of requests it gets hit with. This make the work easier for whoever is consuming your API. Additionally it gives a much experience while developing.
Create a new class ApiResponse and copy down the following.
public class ApiResponse<T> { public T Data { get; set; } public bool Succeeded { get; set; } public string Message { get; set; } public static ApiResponse<T> Fail(string errorMessage) { return new ApiResponse<T> { Succeeded = false, Message = errorMessage }; } public static ApiResponse<T> Success(T data) { return new ApiResponse<T> { Succeeded = true, Data = data }; } }
The ApiResponse class is of a generic type, meaning any kind of data can be passed along with it. Data property will hold the actual data returned from the server. Message contains any Exceptions or Info message in string type. And finally there is a boolean that denotes if the request is a success. You can add multiple other properties as well depending on your requirement.
We also have Fail and Success method that is built specifically for our Exception handling scenario. You can find how this is being used in the upcoming sections.
As mentioned earlier, let’s also create a custom exception. Create a new class and name it SomeException.cs or anything. Make sure that you inherit Exception as the base class. Here is how the custom exception looks like.
public class SomeException : Exception { public SomeException() : base() { } public SomeException(string message) : base(message) { } public SomeException(string message, params object[] args) : base(String.Format(CultureInfo.CurrentCulture, message, args)) { } }
Here is how you would be using this Custom Exception class that we created now.
throw new SomeException("An error occurred...");
Get the idea, right? In this way you can actually differentiate between exceptions. To get even more clarity related to this scenario, let’s say we have other custom exceptions like ProductNotFoundException , StockExpiredException, CustomerInvalidException and so on. Just give some meaningful names so that you can easily identify. Now you can use these exception classes wherever the specific exception arises. This sends the related exception to the middleware, which has logics to handle it.
Now, let’s create the Global Exception Handling Middleware. Create a new class and name it ErrorHandlerMiddleware.cs
public class ErrorHandlerMiddleware { private readonly RequestDelegate _next; public ErrorHandlerMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext context) { try { await _next(context); } catch (Exception error) { var response = context.Response; response.ContentType = "application/json"; var responseModel = ApiResponse<string>.Fail(error.Message); switch (error) { case SomeException e: // custom application error response.StatusCode = (int)HttpStatusCode.BadRequest; break; case KeyNotFoundException e: // not found error response.StatusCode = (int)HttpStatusCode.NotFound; break; default: // unhandled error response.StatusCode = (int)HttpStatusCode.InternalServerError; break; } var result = JsonSerializer.Serialize(responseModel); await response.WriteAsync(result); } } }
Line 3 – RequestDelegate denotes a HTTP Request completion.
Line 10 – A simple try-catch block over the request delegate. It means that whenever there is an exception of any type in the pipeline for the current request, control goes to the catch block. In this middleware, Catch block has all the goodness.
Line 14 – Catches all the Exceptions. Remember, all our custom exceptions are derived from the Exception base class.
Line 18 – Creates an APIReponse Model out of the error message using the Fail method that we created earlier.
Line 21 – In case the caught exception is of type SomeException, the status code is set to BadRequest. You get the idea, yeah? The other exceptions are also handled in a similar fashion.
Line 34 – Finally, the created api-response model is serialized and send as a response.
Before running this implementation, make sure that you don’t miss adding this middleware to the application pipeline. Open up the Startup.cs / Configure method and add in the following line.
app.UseMiddleware<ErrorHandlerMiddleware>();
Make sure that you comment out or delete the UseExceptionHandler default middleware as it may cause unwanted clashes. It doesn’t make sense to have multiple middlewares doing the same thing, yeah?
I also assume that you have done the necessary changes that will throw the SomeException Exception in the Get method of the default controller you are working with.
With that done, let’s run the application and see how the error get’s displayed on Swagger.
There you go! You can see how well built the response is and how easy it is to read what the API has to say to the client. Now, we have a completely custom-built error handling mechanism, all in one place. And yes, of course as mentioned earlier, you are always free to add more properties to the API Reponses class that suits your application’s needs.
I have been using this approach for literally all of my open source projects, and it’s With that, let’s wrap up the article for now 😉
Consider supporting me by buying me a coffee.
Thank you for visiting. You can buy me a coffee by clicking the button below. Cheers!
Summary
In this article, we have looked through various ways to implement Exception handling in our ASP.NET Core applications. The favorite approach should definitely be the one where we implemented Global Exception Handling in ASP.NET Core using Custom Middlewares. You can also find the complete source code on my Github here. Have any suggestions or questions? Feel free to leave them in the comments section below. Thanks and Happy Coding! 😀