Manejo de excepciones de API web ASP.NET Core

280

Estoy usando ASP.NET Core para mi nuevo proyecto de API REST después de usar ASP.NET Web API durante muchos años. No veo ninguna buena manera de manejar las excepciones en ASP.NET Core Web API. Traté de implementar el filtro / atributo de manejo de excepciones:

public class ErrorHandlingFilter : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        HandleExceptionAsync(context);
        context.ExceptionHandled = true;
    }

    private static void HandleExceptionAsync(ExceptionContext context)
    {
        var exception = context.Exception;

        if (exception is MyNotFoundException)
            SetExceptionResult(context, exception, HttpStatusCode.NotFound);
        else if (exception is MyUnauthorizedException)
            SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
        else if (exception is MyException)
            SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
        else
            SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
    }

    private static void SetExceptionResult(
        ExceptionContext context, 
        Exception exception, 
        HttpStatusCode code)
    {
        context.Result = new JsonResult(new ApiResponse(exception))
        {
            StatusCode = (int)code
        };
    }
}

Y aquí está mi registro de filtro de inicio:

services.AddMvc(options =>
{
    options.Filters.Add(new AuthorizationFilter());
    options.Filters.Add(new ErrorHandlingFilter());
});

El problema que estaba teniendo es que cuando ocurre una excepción en mi AuthorizationFilterno está siendo manejado por ErrorHandlingFilter. Esperaba que fuera atrapado allí al igual que funcionaba con la antigua API web ASP.NET.

Entonces, ¿cómo puedo detectar todas las excepciones de aplicación, así como cualquier excepción de los filtros de acción?

Andrei
fuente
3
¿Has probado el UseExceptionHandlermiddleware?
Pawel
Tengo un ejemplo aquí sobre cómo usar UseExceptionHandlermiddleware
Ilya Chernomordik

Respuestas:

539

Middleware de manejo de excepciones

Después de muchos experimentos con diferentes enfoques de manejo de excepciones, terminé usando middleware. Funcionó mejor para mi aplicación ASP.NET Core Web API. Maneja excepciones de aplicaciones, así como excepciones de filtros de acción y tengo control total sobre el manejo de excepciones y la respuesta HTTP. Aquí está mi excepción en el manejo de middleware:

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate next;
    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected

        if      (ex is MyNotFoundException)     code = HttpStatusCode.NotFound;
        else if (ex is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
        else if (ex is MyException)             code = HttpStatusCode.BadRequest;

        var result = JsonConvert.SerializeObject(new { error = ex.Message });
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;
        return context.Response.WriteAsync(result);
    }
}

Regístralo antes de MVC en Startupclase:

app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();

Puede agregar seguimiento de pila, nombre de tipo de excepción, códigos de error o cualquier cosa que desee. Muy flexible. Aquí hay un ejemplo de respuesta de excepción:

{ "error": "Authentication token is not valid." }

Considere inyectar IOptions<MvcJsonOptions>en el Invokemétodo para luego usarlo cuando serialice el objeto de respuesta para utilizar la configuración de serialización de ASP.NET MVC JsonConvert.SerializeObject(errorObj, opts.Value.SerializerSettings)para una mejor consistencia de serialización en todos los puntos finales.

Enfoque 2

Hay otra API no obvia llamada UseExceptionHandlerque funciona "bien" para escenarios simples:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var feature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = feature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

Esta no es una manera muy obvia pero fácil de configurar el manejo de excepciones. Sin embargo, todavía prefiero el enfoque de middleware sobre él, ya que obtengo más control con la capacidad de inyectar las dependencias necesarias.

Andrei
fuente
44
He estado golpeándome la cabeza contra el escritorio tratando de hacer que un middleware personalizado funcione hoy, y funciona básicamente de la misma manera (lo estoy usando para administrar la unidad de trabajo / transacción para una solicitud). El problema al que me enfrento es que las excepciones planteadas en 'next' no quedan atrapadas en el middleware. Como puedes imaginar, esto es problemático. ¿Qué estoy haciendo mal / falta? ¿Alguna sugerencia o sugerencia?
brappleye3
55
@ brappleye3: descubrí cuál era el problema. Estaba registrando el middleware en el lugar incorrecto en la clase Startup.cs. Me mudé app.UseMiddleware<ErrorHandlingMiddleware>();a justo antes app.UseStaticFiles();. La excepción parece haberse captado correctamente ahora. Esto me lleva a creer app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); app.UseBrowserLink();Hacer algo de hacker interno de middleware mágico para que el middleware ordene correctamente.
Jamadan
44
Estoy de acuerdo en que el middleware personalizado puede ser muy útil, pero cuestionaría el uso de excepciones para situaciones NotFound, No autorizado y BadRequest. ¿Por qué no simplemente establece el código de estado (usando NotFound (), etc.) y luego lo maneja en su middleware personalizado o mediante UseStatusCodePagesWithReExecute? Ver devtrends.co.uk/blog/handling-errors-in-asp.net-core-web-api para más información
Paul Hiles
44
Es malo porque siempre está serializando a JSON, ignorando por completo la negociación de contenido.
Konrad
55
@Konrad punto válido. Es por eso que dije que este ejemplo es donde puedes comenzar, y no el resultado final. Para el 99% de las API, JSON es más que suficiente. Si cree que esta respuesta no es lo suficientemente buena, no dude en contribuir.
Andrei
61

Lo último Asp.Net Core(al menos desde 2.2, probablemente antes) tiene un middleware incorporado que lo hace un poco más fácil en comparación con la implementación en la respuesta aceptada:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

Debería hacer más o menos lo mismo, solo un poco menos de código para escribir.

Importante: Recuerde agregarlo antes UseMvc(o UseRoutingen .Net Core 3) ya que el orden es importante.

Ilya Chernomordik
fuente
¿Es compatible con DI como un argumento para el controlador, o tendría que usar un patrón de localización de servicios dentro del controlador?
lp
33

Su mejor opción es usar middleware para lograr el registro que está buscando. Desea colocar su registro de excepciones en un middleware y luego manejar sus páginas de error que se muestran al usuario en un middleware diferente. Eso permite la separación de la lógica y sigue el diseño que Microsoft ha presentado con los 2 componentes de middleware. Aquí hay un buen enlace a la documentación de Microsoft: Manejo de errores en ASP.Net Core

Para su ejemplo específico, es posible que desee utilizar una de las extensiones en el middleware StatusCodePage o enrollar la suya de esta manera .

Puede encontrar un ejemplo aquí para registrar excepciones: ExceptionHandlerMiddleware.cs

public void Configure(IApplicationBuilder app)
{
    // app.UseErrorPage(ErrorPageOptions.ShowAll);
    // app.UseStatusCodePages();
    // app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
    // app.UseStatusCodePages("text/plain", "Response, status code: {0}");
    // app.UseStatusCodePagesWithRedirects("~/errors/{0}");
    // app.UseStatusCodePagesWithRedirects("/base/errors/{0}");
    // app.UseStatusCodePages(builder => builder.UseWelcomePage());
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");  // I use this version

    // Exception handling logging below
    app.UseExceptionHandler();
}

Si no le gusta esa implementación específica, también puede usar ELM Middleware , y aquí hay algunos ejemplos: Elm Exception Middleware

public void Configure(IApplicationBuilder app)
{
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");
    // Exception handling logging below
    app.UseElmCapture();
    app.UseElmPage();
}

Si eso no funciona para sus necesidades, siempre puede rodar su propio componente de Middleware mirando sus implementaciones de ExceptionHandlerMiddleware y ElmMiddleware para comprender los conceptos para construir el suyo propio.

Es importante agregar el middleware de manejo de excepciones debajo del middleware StatusCodePages pero sobre todos los demás componentes de middleware. De esa forma, su middleware Exception capturará la excepción, la registrará y luego permitirá que la solicitud continúe con el middleware StatusCodePage, que mostrará la página de error amigable al usuario.

Ashley Lee
fuente
De nada. También proporcioné un enlace a un ejemplo para anular las páginas UseStatusPages predeterminadas en casos extremos que pueden satisfacer mejor su solicitud.
Ashley Lee
1
Tenga en cuenta que Elm no conserva los registros, y se recomienda utilizar Serilog o NLog para proporcionar la serialización. Ver registros de ELM desaparece. ¿Podemos persistirlo en un archivo o DB?
Michael Freidgeim
2
El enlace ahora está roto.
Mathias Lykkegaard Lorenzen
@AshleyLee, cuestiono que UseStatusCodePagessea ​​útil en implementaciones de servicios de API web. No hay vistas ni HTML, solo respuestas JSON ...
Paul Michalik
23

La respuesta bien aceptada me ayudó mucho, pero quería pasar HttpStatusCode en mi middleware para administrar el código de estado de error en tiempo de ejecución.

De acuerdo con este enlace, tengo una idea para hacer lo mismo. Así que fusioné la respuesta de Andrei con esto. Entonces mi código final está abajo:
1. Clase base

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}

2. Tipo de clase de excepción personalizada

 public class HttpStatusCodeException : Exception
{
    public HttpStatusCode StatusCode { get; set; }
    public string ContentType { get; set; } = @"text/plain";

    public HttpStatusCodeException(HttpStatusCode statusCode)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, string message) : base(message)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, Exception inner) : this(statusCode, inner.ToString()) { }

    public HttpStatusCodeException(HttpStatusCode statusCode, JObject errorObject) : this(statusCode, errorObject.ToString())
    {
        this.ContentType = @"application/json";
    }

}


3. Middleware de excepción personalizado

public class CustomExceptionMiddleware
    {
        private readonly RequestDelegate next;

    public CustomExceptionMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (HttpStatusCodeException ex)
        {
            await HandleExceptionAsync(context, ex);
        }
        catch (Exception exceptionObj)
        {
            await HandleExceptionAsync(context, exceptionObj);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception)
    {
        string result = null;
        context.Response.ContentType = "application/json";
        if (exception is HttpStatusCodeException)
        {
            result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)exception.StatusCode }.ToString();
            context.Response.StatusCode = (int)exception.StatusCode;
        }
        else
        {
            result = new ErrorDetails() { Message = "Runtime Error", StatusCode = (int)HttpStatusCode.BadRequest }.ToString();
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        }
        return context.Response.WriteAsync(result);
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        string result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)HttpStatusCode.InternalServerError }.ToString();
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return context.Response.WriteAsync(result);
    }
}


4. Método de extensión

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
    {
        app.UseMiddleware<CustomExceptionMiddleware>();
    }

5. Configure el método en startup.cs

app.ConfigureCustomExceptionMiddleware();
app.UseMvc();

Ahora mi método de inicio de sesión en el controlador de cuenta:

 try
        {
            IRepository<UserMaster> obj = new Repository<UserMaster>(_objHeaderCapture, Constants.Tables.UserMaster);
            var Result = obj.Get().AsQueryable().Where(sb => sb.EmailId.ToLower() == objData.UserName.ToLower() && sb.Password == objData.Password.ToEncrypt() && sb.Status == (int)StatusType.Active).FirstOrDefault();
            if (Result != null)//User Found
                return Result;
            else// Not Found
                throw new HttpStatusCodeException(HttpStatusCode.NotFound, "Please check username or password");
        }
        catch (Exception ex)
        {
            throw ex;
        }

Arriba puede ver si no he encontrado al usuario y luego levantar la HttpStatusCodeException en la que he pasado el estado HttpStatusCode.NotFound y un mensaje personalizado
en el middleware

catch (HttpStatusCodeException ex)

bloqueado se llamará que pasará el control a

Método privado Task HandleExceptionAsync (contexto HttpContext, excepción HttpStatusCodeException)

.


Pero, ¿qué pasa si tengo un error de tiempo de ejecución antes? Para eso he usado el bloque try catch que arroja la excepción y será atrapado en el bloque catch (Exception exceptionObj) y pasaré el control a

Task HandleExceptionAsync (contexto HttpContext, excepción de excepción)

método.

He usado una sola clase ErrorDetails para uniformidad.

Arjun
fuente
¿Dónde poner el método de extensión? Por desgracia en el startup.csde void Configure(IapplicationBuilder app)Me aparece un error IApplicationBuilder does not contain a definition for ConfigureCustomExceptionMiddleware. Y agregué la referencia, donde CustomExceptionMiddleware.csestá.
Spedo De La Rossa
no desea utilizar excepciones, ya que ralentizan sus apis. Las excepciones son muy caras.
lnaie
@Inaie, no puedo decir sobre eso ... pero parece que nunca has tenido ninguna excepción para manejar ... Gran trabajo
Arjun
19

Para configurar el comportamiento de manejo de excepciones por tipo de excepción, puede usar Middleware de paquetes NuGet:

Código de muestra:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddExceptionHandlingPolicies(options =>
    {
        options.For<InitializationException>().Rethrow();

        options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();

        options.For<SomeBadRequestException>()
        .Response(e => 400)
            .Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
            .WithBody((req,sw, exception) =>
                {
                    byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
                    return sw.WriteAsync(array, 0, array.Length);
                })
        .NextPolicy();

        // Ensure that all exception types are handled by adding handler for generic exception at the end.
        options.For<Exception>()
        .Log(lo =>
            {
                lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
                lo.Category = (context, exception) => "MyCategory";
            })
        .Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
            .ClearCacheHeaders()
            .WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
        .Handled();
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseExceptionHandlingPolicies();
    app.UseMvc();
}
Ihar Yakimush
fuente
16

En primer lugar, gracias a Andrei porque he basado mi solución en su ejemplo.

Incluyo el mío ya que es una muestra más completa y podría ahorrarle algo de tiempo a los lectores.

La limitación del enfoque de Andrei es que no maneja el registro, capturando variables de solicitud potencialmente útiles y negociación de contenido (siempre devolverá JSON sin importar lo que el cliente haya solicitado: XML / texto plano, etc.).

Mi enfoque es usar un ObjectResult que nos permite usar la funcionalidad integrada en MVC.

Este código también evita el almacenamiento en caché de la respuesta.

La respuesta de error se ha decorado de tal manera que el serializador XML puede serializarla.

public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate next;
    private readonly IActionResultExecutor<ObjectResult> executor;
    private readonly ILogger logger;
    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public ExceptionHandlerMiddleware(RequestDelegate next, IActionResultExecutor<ObjectResult> executor, ILoggerFactory loggerFactory)
    {
        this.next = next;
        this.executor = executor;
        logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, $"An unhandled exception has occurred while executing the request. Url: {context.Request.GetDisplayUrl()}. Request Data: " + GetRequestData(context));

            if (context.Response.HasStarted)
            {
                throw;
            }

            var routeData = context.GetRouteData() ?? new RouteData();

            ClearCacheHeaders(context.Response);

            var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

            var result = new ObjectResult(new ErrorResponse("Error processing request. Server error."))
            {
                StatusCode = (int) HttpStatusCode.InternalServerError,
            };

            await executor.ExecuteAsync(actionContext, result);
        }
    }

    private static string GetRequestData(HttpContext context)
    {
        var sb = new StringBuilder();

        if (context.Request.HasFormContentType && context.Request.Form.Any())
        {
            sb.Append("Form variables:");
            foreach (var x in context.Request.Form)
            {
                sb.AppendFormat("Key={0}, Value={1}<br/>", x.Key, x.Value);
            }
        }

        sb.AppendLine("Method: " + context.Request.Method);

        return sb.ToString();
    }

    private static void ClearCacheHeaders(HttpResponse response)
    {
        response.Headers[HeaderNames.CacheControl] = "no-cache";
        response.Headers[HeaderNames.Pragma] = "no-cache";
        response.Headers[HeaderNames.Expires] = "-1";
        response.Headers.Remove(HeaderNames.ETag);
    }

    [DataContract(Name= "ErrorResponse")]
    public class ErrorResponse
    {
        [DataMember(Name = "Message")]
        public string Message { get; set; }

        public ErrorResponse(string message)
        {
            Message = message;
        }
    }
}
CountZero
fuente
9

Primero, configure ASP.NET Core 2 Startuppara volver a ejecutar en una página de error cualquier error del servidor web y cualquier excepción no controlada.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment()) {
        // Debug config here...
    } else {
        app.UseStatusCodePagesWithReExecute("/Error");
        app.UseExceptionHandler("/Error");
    }
    // More config...
}

A continuación, defina un tipo de excepción que le permitirá generar errores con códigos de estado HTTP.

public class HttpException : Exception
{
    public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
    public HttpStatusCode StatusCode { get; private set; }
}

Finalmente, en su controlador para la página de error, personalice la respuesta según el motivo del error y si la respuesta será vista directamente por un usuario final. Este código supone que todas las URL de API comienzan con /api/.

[AllowAnonymous]
public IActionResult Error()
{
    // Gets the status code from the exception or web server.
    var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
        httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;

    // For API errors, responds with just the status code (no page).
    if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
        return StatusCode((int)statusCode);

    // Creates a view model for a user-friendly error page.
    string text = null;
    switch (statusCode) {
        case HttpStatusCode.NotFound: text = "Page not found."; break;
        // Add more as desired.
    }
    return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}

ASP.NET Core registrará los detalles del error para que pueda depurarlos, por lo que un código de estado puede ser todo lo que desea proporcionar a un solicitante (potencialmente no confiable). Si desea mostrar más información, puede mejorar HttpExceptionpara proporcionarla. Para errores de API, puede colocar información de error codificada con JSON en el cuerpo del mensaje reemplazándola return StatusCode...por return Json....

Edward Brey
fuente
0

use middleware o IExceptionHandlerPathFeature está bien. hay otra manera en eshop

crear un filtro de excepción y registrarlo

public class HttpGlobalExceptionFilter : IExceptionFilter
{
  public void OnException(ExceptionContext context)
  {...}
}
services.AddMvc(options =>
{
  options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})
ws_
fuente