Control de errores en API web de ASP.NET Core

En este artículo se describe cómo administrar errores y personalizar el control de errores con API web de ASP.NET Core.

Página de excepciones para el desarrollador

La página de excepciones para el desarrollador muestra seguimientos de pila detallados de los errores del servidor. Usa DeveloperExceptionPageMiddleware para capturar excepciones sincrónicas y asincrónicas de la canalización HTTP y para generar respuestas de error. Por ejemplo, considere la siguiente acción del controlador, que produce una excepción:

[HttpGet("Throw")]
public IActionResult Throw() =>
    throw new Exception("Sample exception.");

Cuando la Página de excepciones para desarrolladores detecta una excepción no controlada, genera de manera predeterminada una respuesta en texto sin formato similar a la del siguiente ejemplo:

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()

...

Si el cliente solicita una respuesta con formato HTML, la Página de excepciones para desarrolladores genera una respuesta similar a la del siguiente ejemplo:

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;
}

...

Para solicitar una respuesta con formato HTML, establezca el encabezado de solicitud Accept en text/html.

Advertencia

No habilite la página de excepciones para el desarrollador a menos que la aplicación se ejecute en el entorno de desarrollo. No comparta públicamente información detallada sobre las excepciones cuando la aplicación se ejecute en producción. Para más información sobre la configuración de los entornos, consulte Uso de varios entornos en ASP.NET Core.

Controlador de excepciones

En entornos que no son de desarrollo, use el middleware de control de excepciones para producir una carga de error:

  1. En Program.cs, llame a UseExceptionHandler para agregar el middleware de control de excepciones:

    var app = builder.Build();
    
    app.UseHttpsRedirection();
    
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error");
    }
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    
  2. Configure una acción de controlador para responder a la ruta /error:

    [Route("/error")]
    public IActionResult HandleError() =>
        Problem();
    

La acción HandleError anterior envía una carga compatible con RFC 7807 al cliente.

Advertencia

No marque el método de acción del controlador de errores con atributos de método HTTP, como HttpGet. Los verbos explícitos impiden que algunas solicitudes lleguen al método de acción.

Para las API web que usan Swagger /OpenAPI, marque la acción del controlador de errores con el atributo [ApiExplorerSettings] y establezca su propiedad IgnoreApi en true. Esta configuración de atributo excluye la acción del controlador de errores de la especificación OpenAPI de la aplicación:

[ApiExplorerSettings(IgnoreApi = true)]

Permita el acceso anónimo al método si los usuarios no autenticados deben recibir el error.

El middleware de control de excepciones también se puede usar en el entorno de desarrollo para generar un formato de carga coherente en todos los entornos:

  1. En Program.cs, registre las instancias de middleware de control de excepciones específicas del entorno:

    if (app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error-development");
    }
    else
    {
        app.UseExceptionHandler("/error");
    }
    

    En el código anterior, el middleware se ha registrado con:

    • Una ruta de /error-development en el entorno de desarrollo.
    • Una ruta de /error en entornos que no son de desarrollo.

  2. Agregue acciones de controlador tanto para las rutas de desarrollo como para las que no son de desarrollo:

    [Route("/error-development")]
    public IActionResult HandleErrorDevelopment(
        [FromServices] IHostEnvironment hostEnvironment)
    {
        if (!hostEnvironment.IsDevelopment())
        {
            return NotFound();
        }
    
        var exceptionHandlerFeature =
            HttpContext.Features.Get<IExceptionHandlerFeature>()!;
    
        return Problem(
            detail: exceptionHandlerFeature.Error.StackTrace,
            title: exceptionHandlerFeature.Error.Message);
    }
    
    [Route("/error")]
    public IActionResult HandleError() =>
        Problem();
    

Uso de excepciones para modificar la respuesta

El contenido de la respuesta se puede modificar desde fuera del controlador mediante una excepción personalizada y un filtro de acción:

  1. Cree un tipo de excepción conocido denominado HttpResponseException:

    public class HttpResponseException : Exception
    {
        public HttpResponseException(int statusCode, object? value = null) =>
            (StatusCode, Value) = (statusCode, value);
    
        public int StatusCode { get; }
    
        public object? Value { get; }
    }
    
  2. Cree un filtro de acción denominado HttpResponseExceptionFilter:

    public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
    {
        public int Order => int.MaxValue - 10;
    
        public void OnActionExecuting(ActionExecutingContext context) { }
    
        public void OnActionExecuted(ActionExecutedContext context)
        {
            if (context.Exception is HttpResponseException httpResponseException)
            {
                context.Result = new ObjectResult(httpResponseException.Value)
                {
                    StatusCode = httpResponseException.StatusCode
                };
    
                context.ExceptionHandled = true;
            }
        }
    }
    

    El filtro anterior especifica un Order del valor entero máximo menos 10. Este Order permite que otros filtros se ejecuten al final de la canalización.

  3. En Program.cs, agregue el filtro de acción a la colección de filtros:

    builder.Services.AddControllers(options =>
    {
        options.Filters.Add<HttpResponseExceptionFilter>();
    });
    

Respuesta de error ante errores de validación

En el caso de los controladores de API web, MVC responde con un tipo de respuesta ValidationProblemDetails cuando se produce un error en la validación del modelo. MVC usa los resultados de InvalidModelStateResponseFactory para construir la respuesta de error para un error de validación. El siguiente ejemplo reemplaza la fábrica predeterminada por una implementación que también es compatible con el formato de respuestas como XML, en Program.cs:

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
            new BadRequestObjectResult(context.ModelState)
            {
                ContentTypes =
                {
                    // using static System.Net.Mime.MediaTypeNames;
                    Application.Json,
                    Application.Xml
                }
            };
    })
    .AddXmlSerializerFormatters();

Respuesta de error del cliente

Se define un resultado error como un resultado con un código de estado HTTP de 400 o superior. Para los controladores de la API web, MVC transforma un resultado de error para producir un ProblemDetails.

La creación automática de un ProblemDetails para los códigos de estado de error está habilitada de manera predeterminada, pero las respuestas de error pueden configurarse de una de las siguientes formas:

  1. Usar el servicio de detalles del problema
  2. Implementar ProblemDetailsFactory
  3. Usar ApiBehaviorOptions.ClientErrorMapping

Respuesta de detalles del problema predeterminada

El siguiente archivo Program.cs fue generado por las plantillas de aplicación web para controladores de API:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Considere el controlador siguiente, que devuelve BadRequest cuando la entrada no es válida:

[Route("api/[controller]/[action]")]
[ApiController]
public class Values2Controller : ControllerBase
{
    // /api/values2/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values2 /squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }
}

Se genera una respuesta de detalles del problema con el código anterior cuando se aplica cualquiera de las condiciones siguientes:

  • Se llama al punto de conexión de /api/values2/divide con denominador cero.
  • Se llama al punto de conexión de /api/values2/squareroot con un radicando menor que cero.

El cuerpo de la respuesta de detalles del problema predeterminado tiene los siguientes valores type, title y status:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "Bad Request",
  "status": 400,
  "traceId": "00-84c1fd4063c38d9f3900d06e56542d48-85d1d4-00"
}

Servicio de detalles del problema

ASP.NET Core admite la creación de detalles del problema para las API HTTP mediante IProblemDetailsService. Para más información, consulte el Servicio de detalles del problema.

El código siguiente configura la aplicación para generar una respuesta de detalles del problema para todas las respuestas de error de servidor y cliente HTTP que aún no tienen contenido en el cuerpo:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();
app.Run();

Considere el controlador de API de la sección anterior, que devuelve BadRequest cuando la entrada no es válida:

[Route("api/[controller]/[action]")]
[ApiController]
public class Values2Controller : ControllerBase
{
    // /api/values2/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values2 /squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }
}

Se genera una respuesta de detalles del problema con el código anterior cuando se aplica cualquiera de las condiciones siguientes:

  • Se proporciona una entrada no válida.
  • El URI no tiene ningún punto de conexión coincidente.
  • Se produce una excepción no controlada.

La creación automática de ProblemDetails para los códigos de estado de error está deshabilitada cuando la propiedad SuppressMapClientErrors está establecida en true:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressMapClientErrors = true;
    });

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Usando el código anterior, cuando un controlador de API devuelve BadRequest, se devuelve un estado de respuesta HTTP 400 sin cuerpo de respuesta. SuppressMapClientErrors evita que se cree una respuesta ProblemDetails, incluso cuando se llama a WriteAsync para un punto de conexión del controlador de API. WriteAsync se explica posteriormente en este artículo.

La siguiente sección muestra cómo personalizar el cuerpo de respuesta de los detalles del problema, usando CustomizeProblemDetails, para devolver una respuesta más útil. Para más opciones de personalización, consulte Personalización de los detalles del problema.

Personalización de los detalles del problema con CustomizeProblemDetails

El código siguiente usa ProblemDetailsOptions para establecer CustomizeProblemDetails:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails(options =>
        options.CustomizeProblemDetails = (context) =>
        {

            var mathErrorFeature = context.HttpContext.Features
                                                       .Get<MathErrorFeature>();
            if (mathErrorFeature is not null)
            {
                (string Detail, string Type) details = mathErrorFeature.MathError switch
                {
                    MathErrorType.DivisionByZeroError =>
                    ("Divison by zero is not defined.",
                                          "https://wikipedia.org/wiki/Division_by_zero"),
                    _ => ("Negative or complex numbers are not valid input.",
                                          "https://wikipedia.org/wiki/Square_root")
                };

                context.ProblemDetails.Type = details.Type;
                context.ProblemDetails.Title = "Bad Input";
                context.ProblemDetails.Detail = details.Detail;
            }
        }
    );

var app = builder.Build();

app.UseHttpsRedirection();

app.UseStatusCodePages();

app.UseAuthorization();

app.MapControllers();

app.Run();

El controlador de API actualizado:

[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // /api/values/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }

}

El siguiente código contiene MathErrorFeature y MathErrorType, que se usan con el ejemplo anterior:

// Custom Http Request Feature
class MathErrorFeature
{
    public MathErrorType MathError { get; set; }
}

// Custom math errors
enum MathErrorType
{
    DivisionByZeroError,
    NegativeRadicandError
}

Se genera una respuesta de detalles del problema con el código anterior cuando se aplica cualquiera de las condiciones siguientes:

  • Se llama al punto de conexión de /divide con denominador cero.
  • Se llama al punto de conexión de /squareroot con un radicando menor que cero.
  • El URI no tiene ningún punto de conexión coincidente.

El cuerpo de la respuesta de detalles del problema contiene lo siguiente cuando se llama a cualquiera de los puntos de conexión de squareroot con un radicando menor que cero:

{
  "type": "https://en.wikipedia.org/wiki/Square_root",
  "title": "Bad Input",
  "status": 400,
  "detail": "Negative or complex numbers are not allowed."
}

Ver o descargar el código de ejemplo

Implemente ProblemDetailsFactory

MVC usa Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory para generar todas las instancias de ProblemDetails y ValidationProblemDetails. Esta fábrica se usa para:

Para personalizar la respuesta de detalles del problema, registre una implementación personalizada de ProblemDetailsFactory en Program.cs:

builder.Services.AddControllers();
builder.Services.AddTransient<ProblemDetailsFactory, SampleProblemDetailsFactory>();

Use ApiBehaviorOptions.ClientErrorMapping

Use la propiedad ClientErrorMapping para configurar el contenido de la respuesta ProblemDetails. Por ejemplo, el código siguiente de Program.cs permite actualizar la propiedad Link para respuestas 404:

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });

Recursos adicionales

En este artículo se describe cómo administrar errores y personalizar el control de errores con API web de ASP.NET Core.

Página de excepciones para el desarrollador

La página de excepciones para el desarrollador muestra seguimientos de pila detallados de los errores del servidor. Usa DeveloperExceptionPageMiddleware para capturar excepciones sincrónicas y asincrónicas de la canalización HTTP y para generar respuestas de error. Por ejemplo, considere la siguiente acción del controlador, que produce una excepción:

[HttpGet("Throw")]
public IActionResult Throw() =>
    throw new Exception("Sample exception.");

Cuando la Página de excepciones para desarrolladores detecta una excepción no controlada, genera de manera predeterminada una respuesta en texto sin formato similar a la del siguiente ejemplo:

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()

...

Si el cliente solicita una respuesta con formato HTML, la Página de excepciones para desarrolladores genera una respuesta similar a la del siguiente ejemplo:

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;
}

...

Para solicitar una respuesta con formato HTML, establezca el encabezado de solicitud Accept en text/html.

Advertencia

No habilite la página de excepciones para el desarrollador a menos que la aplicación se ejecute en el entorno de desarrollo. No comparta públicamente información detallada sobre las excepciones cuando la aplicación se ejecute en producción. Para más información sobre la configuración de los entornos, consulte Uso de varios entornos en ASP.NET Core.

Controlador de excepciones

En entornos que no son de desarrollo, use el middleware de control de excepciones para producir una carga de error:

  1. En Program.cs, llame a UseExceptionHandler para agregar el middleware de control de excepciones:

    var app = builder.Build();
    
    app.UseHttpsRedirection();
    
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error");
    }
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    
  2. Configure una acción de controlador para responder a la ruta /error:

    [Route("/error")]
    public IActionResult HandleError() =>
        Problem();
    

La acción HandleError anterior envía una carga compatible con RFC 7807 al cliente.

Advertencia

No marque el método de acción del controlador de errores con atributos de método HTTP, como HttpGet. Los verbos explícitos impiden que algunas solicitudes lleguen al método de acción.

Para las API web que usan Swagger /OpenAPI, marque la acción del controlador de errores con el atributo [ApiExplorerSettings] y establezca su propiedad IgnoreApi en true. Esta configuración de atributo excluye la acción del controlador de errores de la especificación OpenAPI de la aplicación:

[ApiExplorerSettings(IgnoreApi = true)]

Permita el acceso anónimo al método si los usuarios no autenticados deben recibir el error.

El middleware de control de excepciones también se puede usar en el entorno de desarrollo para generar un formato de carga coherente en todos los entornos:

  1. En Program.cs, registre las instancias de middleware de control de excepciones específicas del entorno:

    if (app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error-development");
    }
    else
    {
        app.UseExceptionHandler("/error");
    }
    

    En el código anterior, el middleware se ha registrado con:

    • Una ruta de /error-development en el entorno de desarrollo.
    • Una ruta de /error en entornos que no son de desarrollo.

  2. Agregue acciones de controlador tanto para las rutas de desarrollo como para las que no son de desarrollo:

    [Route("/error-development")]
    public IActionResult HandleErrorDevelopment(
        [FromServices] IHostEnvironment hostEnvironment)
    {
        if (!hostEnvironment.IsDevelopment())
        {
            return NotFound();
        }
    
        var exceptionHandlerFeature =
            HttpContext.Features.Get<IExceptionHandlerFeature>()!;
    
        return Problem(
            detail: exceptionHandlerFeature.Error.StackTrace,
            title: exceptionHandlerFeature.Error.Message);
    }
    
    [Route("/error")]
    public IActionResult HandleError() =>
        Problem();
    

Uso de excepciones para modificar la respuesta

El contenido de la respuesta se puede modificar desde fuera del controlador mediante una excepción personalizada y un filtro de acción:

  1. Cree un tipo de excepción conocido denominado HttpResponseException:

    public class HttpResponseException : Exception
    {
        public HttpResponseException(int statusCode, object? value = null) =>
            (StatusCode, Value) = (statusCode, value);
    
        public int StatusCode { get; }
    
        public object? Value { get; }
    }
    
  2. Cree un filtro de acción denominado HttpResponseExceptionFilter:

    public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
    {
        public int Order => int.MaxValue - 10;
    
        public void OnActionExecuting(ActionExecutingContext context) { }
    
        public void OnActionExecuted(ActionExecutedContext context)
        {
            if (context.Exception is HttpResponseException httpResponseException)
            {
                context.Result = new ObjectResult(httpResponseException.Value)
                {
                    StatusCode = httpResponseException.StatusCode
                };
    
                context.ExceptionHandled = true;
            }
        }
    }
    

    El filtro anterior especifica un Order del valor entero máximo menos 10. Este Order permite que otros filtros se ejecuten al final de la canalización.

  3. En Program.cs, agregue el filtro de acción a la colección de filtros:

    builder.Services.AddControllers(options =>
    {
        options.Filters.Add<HttpResponseExceptionFilter>();
    });
    

Respuesta de error ante errores de validación

En el caso de los controladores de API web, MVC responde con un tipo de respuesta ValidationProblemDetails cuando se produce un error en la validación del modelo. MVC usa los resultados de InvalidModelStateResponseFactory para construir la respuesta de error para un error de validación. El siguiente ejemplo reemplaza la fábrica predeterminada por una implementación que también es compatible con el formato de respuestas como XML, en Program.cs:

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
            new BadRequestObjectResult(context.ModelState)
            {
                ContentTypes =
                {
                    // using static System.Net.Mime.MediaTypeNames;
                    Application.Json,
                    Application.Xml
                }
            };
    })
    .AddXmlSerializerFormatters();

Respuesta de error del cliente

Se define un resultado error como un resultado con un código de estado HTTP de 400 o superior. Para los controladores de la API web, MVC transforma un resultado de error para producir un ProblemDetails.

La respuesta de error se puede configurar de una de las siguientes maneras:

  1. Implementar ProblemDetailsFactory
  2. Uso de ApiBehaviorOptions.ClientErrorMapping

Implemente ProblemDetailsFactory

MVC usa Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory para generar todas las instancias de ProblemDetails y ValidationProblemDetails. Esta fábrica se usa para:

Para personalizar la respuesta de detalles del problema, registre una implementación personalizada de ProblemDetailsFactory en Program.cs:

builder.Services.AddControllers();
builder.Services.AddTransient<ProblemDetailsFactory, SampleProblemDetailsFactory>();

Use ApiBehaviorOptions.ClientErrorMapping

Use la propiedad ClientErrorMapping para configurar el contenido de la respuesta ProblemDetails. Por ejemplo, el código siguiente de Program.cs permite actualizar la propiedad Link para respuestas 404:

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });

Middleware personalizado para controlar excepciones

Los valores predeterminados del middleware de control de excepciones funcionan bien para la mayoría de las aplicaciones. En el caso de las aplicaciones que requieren un control de excepciones especializado, considere la posibilidad de personalizar el middleware de control de excepciones.

Producción de una carga de ProblemDetails para excepciones

ASP.NET Core no genera una carga de error estandarizada cuando se produce una excepción no controlada. En escenarios en los que es conveniente devolver una respuesta ProblemDetails estandarizada al cliente, el middleware ProblemDetails se puede usar para asignar excepciones y respuestas 404 a una carga de ProblemDetails . El middleware de control de excepciones también se puede usar para devolver una carga ProblemDetails para excepciones no controladas.

Recursos adicionales

En este artículo se describe cómo administrar y personalizar el control de errores con API web de ASP.NET Core.

Vea o descargue el código de ejemplo (cómo descargarlo)

Página de excepciones para el desarrollador

La Página de excepciones para el desarrollador es una herramienta útil para obtener seguimientos de pila detallados de los errores del servidor. Usa DeveloperExceptionPageMiddleware para capturar excepciones sincrónicas y asincrónicas de la canalización HTTP y para generar respuestas de error. A modo de ejemplo, observe la siguiente acción del controlador:

[HttpGet("{city}")]
public WeatherForecast Get(string city)
{
    if (!string.Equals(city?.TrimEnd(), "Redmond", StringComparison.OrdinalIgnoreCase))
    {
        throw new ArgumentException(
            $"We don't offer a weather forecast for {city}.", nameof(city));
    }
    
    return GetWeather().First();
}

Ejecute el siguiente comando curl para probar la acción anterior:

curl -i https://localhost:5001/weatherforecast/chicago

La Página de excepciones para el desarrollador muestra una respuesta de texto sin formato si el cliente no solicita la salida con formato HTML. Se mostrará la siguiente salida:

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_folder\aspnet\AspNetCore.Docs\aspnetcore\web-api\handle-errors\samples\3.x\Controllers\WeatherForecastController.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

Para mostrar una respuesta con formato HTML, establezca el encabezado de solicitud HTTP Accept en el tipo de medio text/html. Por ejemplo:

curl -i -H "Accept: text/html" https://localhost:5001/weatherforecast/chicago

El siguiente fragmento de la respuesta HTTP es importante:

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;
}

La respuesta con formato HTML resulta útil al realizar pruebas mediante herramientas como Postman. En la siguiente captura de pantalla se muestra tanto el texto sin formato como las respuestas con formato HTML en Postman:

Test the Developer Exception Page in Postman.

Advertencia

Habilite la página de excepciones para el desarrollador solo cuando la aplicación se ejecute en el entorno de desarrollo. No comparta públicamente información detallada sobre las excepciones cuando la aplicación se ejecute en producción. Para más información sobre la configuración de los entornos, consulte Uso de varios entornos en ASP.NET Core.

No marque el método de acción del controlador de errores con atributos de método HTTP, como HttpGet. Los verbos explícitos impiden que algunas solicitudes lleguen al método de acción. Permita el acceso anónimo al método si los usuarios no autenticados deben recibir el error.

Controlador de excepciones

En entornos que no son de desarrollo, se puede usar middleware de control de excepciones para producir una carga de error:

  1. En Startup.Configure, invoque UseExceptionHandler para usar el middleware:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/error");
        }
    
        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  2. Configure una acción de controlador para responder a la ruta /error:

    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error")]
        public IActionResult Error() => Problem();
    }
    

La acción Error anterior envía una carga compatible con RFC 7807 al cliente.

El middleware de control de excepciones también puede proporcionar una salida negociada de contenido más detallada en el entorno de desarrollo local. Use los pasos siguientes para generar un formato de carga coherente en los entornos de desarrollo y producción:

  1. En Startup.Configure, registre las instancias de middleware de control de excepciones específicas del entorno:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseExceptionHandler("/error-local-development");
        }
        else
        {
            app.UseExceptionHandler("/error");
        }
    }
    

    En el código anterior, el middleware se ha registrado con:

    • Una ruta de /error-local-development en el entorno de desarrollo.
    • Una ruta de /error en entornos que no son de desarrollo.

  2. Aplique el enrutamiento de atributos a acciones del controlador:

    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error-local-development")]
        public IActionResult ErrorLocalDevelopment(
            [FromServices] IWebHostEnvironment webHostEnvironment)
        {
            if (webHostEnvironment.EnvironmentName != "Development")
            {
                throw new InvalidOperationException(
                    "This shouldn't be invoked in non-development environments.");
            }
    
            var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
    
            return Problem(
                detail: context.Error.StackTrace,
                title: context.Error.Message);
        }
    
        [Route("/error")]
        public IActionResult Error() => Problem();
    }
    

    El código anterior llama a ControllerBase.Problem para crear una respuesta ProblemDetails.

Uso de excepciones para modificar la respuesta

El contenido de la respuesta se puede modificar desde fuera del controlador. En la API web de ASP.NET 4.x, una manera de hacerlo era usar el tipo HttpResponseException. ASP.NET Core no incluye un tipo equivalente. Se pueden agregar compatibilidad para HttpResponseException con los pasos siguientes:

  1. Cree un tipo de excepción conocido denominado HttpResponseException:

    public class HttpResponseException : Exception
    {
        public int Status { get; set; } = 500;
    
        public object Value { get; set; }
    }
    
  2. Cree un filtro de acción denominado HttpResponseExceptionFilter:

    public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
    {
        public int Order { get; } = int.MaxValue - 10;
    
        public void OnActionExecuting(ActionExecutingContext context) { }
    
        public void OnActionExecuted(ActionExecutedContext context)
        {
            if (context.Exception is HttpResponseException exception)
            {
                context.Result = new ObjectResult(exception.Value)
                {
                    StatusCode = exception.Status,
                };
                context.ExceptionHandled = true;
            }
        }
    }
    

    El filtro anterior especifica un Order del valor entero máximo menos 10. Este Order permite que otros filtros se ejecuten al final de la canalización.

  3. En Startup.ConfigureServices, agregue el filtro de acción a la colección de filtros:

    services.AddControllers(options =>
        options.Filters.Add(new HttpResponseExceptionFilter()));
    

Respuesta de error ante errores de validación

En el caso de los controladores de API web, MVC responde con un tipo de respuesta ValidationProblemDetails cuando se produce un error en la validación del modelo. MVC usa los resultados de InvalidModelStateResponseFactory para construir la respuesta de error para un error de validación. En el ejemplo siguiente se usa la fábrica para cambiar el tipo de respuesta predeterminado a SerializableError en Startup.ConfigureServices:

services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            var result = new BadRequestObjectResult(context.ModelState);

            // TODO: add `using System.Net.Mime;` to resolve MediaTypeNames
            result.ContentTypes.Add(MediaTypeNames.Application.Json);
            result.ContentTypes.Add(MediaTypeNames.Application.Xml);

            return result;
        };
    });

Respuesta de error del cliente

Se define un resultado error como un resultado con un código de estado HTTP de 400 o superior. En el caso de los controladores de API web, MVC transforma un resultado de un error en un resultado con ProblemDetails.

La respuesta de error se puede configurar de una de las siguientes maneras:

  1. Implementar ProblemDetailsFactory
  2. Usar ApiBehaviorOptions.ClientErrorMapping

Implemente ProblemDetailsFactory

MVC usa Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory para generar todas las instancias de ProblemDetails y ValidationProblemDetails. Esta fábrica se usa para:

Para personalizar la respuesta de detalles del problema, registre una implementación personalizada de ProblemDetailsFactory en Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection serviceCollection)
{
    services.AddControllers();
    services.AddTransient<ProblemDetailsFactory, CustomProblemDetailsFactory>();
}

Uso de ApiBehaviorOptions.ClientErrorMapping

Use la propiedad ClientErrorMapping para configurar el contenido de la respuesta ProblemDetails. Por ejemplo, el código siguiente de Startup.ConfigureServices permite actualizar la propiedad type para respuestas 404:

services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
        options.DisableImplicitFromServicesParameters = true;
    });

Middleware personalizado para controlar excepciones

Los valores predeterminados del middleware de control de excepciones funcionan bien para la mayoría de las aplicaciones. En el caso de las aplicaciones que requieren un control de excepciones especializado, considere la posibilidad de personalizar el middleware de control de excepciones.

Producción de una carga de ProblemDetails para excepciones

ASP.NET Core no genera una carga de error estandarizada cuando se produce una excepción no controlada. En escenarios en los que es conveniente devolver una respuesta ProblemDetails estandarizada al cliente, el middleware ProblemDetails se puede usar para asignar excepciones y respuestas 404 a una carga de ProblemDetails . El middleware de control de excepciones también se puede usar para devolver una carga ProblemDetails para excepciones no controladas.