Control de errores en API web de ASP.NET Core

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

Ver o descargar código de ejemplo ( Cómodescargar)

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

En ASP.NET Core 3.0 y versiones posteriores, en la Página de excepciones para el desarrollador se 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:

En ASP.NET Core 2.2 y versiones anteriores, en la Página de excepciones para el desarrollador se muestra una respuesta con formato HTML. Por ejemplo, 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:

Prueba de la Página de excepciones para el desarrollador en 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 información detallada de excepciones públicamente cuando la aplicación se ejecute en producción. Para más información sobre la configuración de entornos, consulte Usar 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 ver 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();
        });
    }
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/error");
            app.UseHsts();
        }
    
        app.UseHttpsRedirection();
        app.UseMvc();
    }
    
  2. Configure una acción de controlador para responder a la ruta /error:

    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error")]
        public IActionResult Error() => Problem();
    }
    
    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error")]
        public ActionResult Error([FromServices] IHostingEnvironment webHostEnvironment)
        {
            var feature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
            var ex = feature?.Error;
            var isDev = webHostEnvironment.IsDevelopment();
            var problemDetails = new ProblemDetails
            {
                Status = (int)HttpStatusCode.InternalServerError,
                Instance = feature?.Path,
                Title = isDev ? $"{ex.GetType().Name}: {ex.Message}" : "An error occurred.",
                Detail = isDev ? ex.StackTrace : null,
            };
    
            return StatusCode(problemDetails.Status.Value, problemDetails);
        }
    }
    

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");
        }
    }
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment 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();
    }
    
    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error-local-development")]
        public IActionResult ErrorLocalDevelopment(
            [FromServices] IHostingEnvironment webHostEnvironment)
        {
            if (!webHostEnvironment.IsDevelopment())
            {
                throw new InvalidOperationException(
                    "This shouldn't be invoked in non-development environments.");
            }
    
            var feature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
            var ex = feature?.Error;
    
            var problemDetails = new ProblemDetails
            {
                Status = (int)HttpStatusCode.InternalServerError,
                Instance = feature?.Path,
                Title = ex.GetType().Name,
                Detail = ex.StackTrace,
            };
    
            return StatusCode(problemDetails.Status.Value, problemDetails);
        }
    
        [Route("/error")]
        public ActionResult Error(
            [FromServices] IHostingEnvironment webHostEnvironment)
        {
            var feature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
            var ex = feature?.Error;
            var isDev = webHostEnvironment.IsDevelopment();
            var problemDetails = new ProblemDetails
            {
                Status = (int)HttpStatusCode.InternalServerError,
                Instance = feature?.Path,
                Title = isDev ? $"{ex.GetType().Name}: {ex.Message}" : "An error occurred.",
                Detail = isDev ? ex.StackTrace : null,
            };
    
            return StatusCode(problemDetails.Status.Value, problemDetails);
        }
    }
    

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

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 valor entero máximo Order menos 10. Esto 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()));
    
    services.AddMvc(options =>
            options.Filters.Add(new HttpResponseExceptionFilter()))
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    
    services.AddMvc(options =>
            options.Filters.Add(new HttpResponseExceptionFilter()))
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    

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;
        };
    });
services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
    .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;
        };
    });
services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = context =>
    {
        var result = new BadRequestObjectResult(context.ModelState);

        // TODO: add `using 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.

Importante

ASP.NET Core 2.1 genera una respuesta con los detalles del problema que es prácticamente compatible con RFC 7807. Si es importante que sea compatible al 100 %, actualice el proyecto a ASP.NET Core 2.2 o posterior.

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. Esto incluye las respuestas de error del cliente, las respuestas de error ante errores de validación y los métodos auxiliares ControllerBase.Problem y ControllerBase.ValidationProblem.

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

La respuesta de error se puede configurar como se describe en la sección Uso de ApiBehaviorOptions.ClientErrorMapping.

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";
    });
services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[404].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.

Generación de una carga problemDetails para excepciones

ASP.NET Core genera una carga de error estandarizada cuando el servidor encuentra 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 404 a una carga problemDetails. El middleware de control de excepciones también se puede usar para devolver una ProblemDetails carga para excepciones no controladas.