Compartir vía


Control global de errores en ASP.NET Web API 2

de David Matson, Rick Anderson

En este tema se proporciona información general sobre el control global de errores en ASP.NET Web API 2 para ASP.NET 4.x. Hoy en día no hay ninguna manera fácil en la API web de registrar o controlar errores globalmente. Algunas excepciones no controladas se pueden procesar a través de filtros de excepciones, pero hay varios casos que los filtros de excepción no pueden controlar. Por ejemplo:

  1. Excepciones iniciadas por constructores del controlador.
  2. Excepciones iniciadas por controladores de mensajes.
  3. Excepciones iniciadas durante el enrutamiento.
  4. Excepciones iniciadas durante la serialización del contenido de respuesta.

Queremos proporcionar una manera sencilla y coherente de registrar y controlar (siempre que sea posible) estas excepciones.

Hay dos casos principales para controlar excepciones, el caso en el que podemos enviar una respuesta de error y el caso en el que todo lo que podemos hacer es registrar la excepción. Un ejemplo para este último caso es cuando se produce una excepción en medio del contenido de la respuesta de streaming; en ese caso, es demasiado tarde enviar un nuevo mensaje de respuesta, ya que el código de estado, los encabezados y el contenido parcial ya han pasado por la conexión, por lo que simplemente anulamos la conexión. Aunque la excepción no se puede controlar para generar un nuevo mensaje de respuesta, todavía se admite el registro de la excepción. En los casos en los que podemos detectar un error, podemos devolver una respuesta de error adecuada, como se muestra en lo siguiente:

public IHttpActionResult GetProduct(int id)
{
    var product = products.FirstOrDefault((p) => p.Id == id);
    if (product == null)
    {
        return NotFound();
    }
    return Ok(product);
}

Opciones existentes

Además de filtros de excepciones, controladores de mensajes se pueden usar hoy para observar todas las respuestas de nivel 500, pero actuar en esas respuestas es difícil, ya que carecen de contexto sobre el error original. Los controladores de mensajes también tienen algunas de las mismas limitaciones que los filtros de excepciones con respecto a los casos que pueden controlar. Aunque la API web tiene infraestructura de seguimiento que captura las condiciones de error que la infraestructura de seguimiento tiene para fines de diagnóstico y no está diseñada ni adecuada para ejecutarse en entornos de producción. El control y el registro globales de excepciones deben ser servicios que se pueden ejecutar durante la producción y conectarse a soluciones de supervisión existentes (por ejemplo, ELMAH).

Información general de la solución

Proporcionamos dos nuevos servicios reemplazables por el usuario, IExceptionLogger e IExceptionHandler, para registrar y controlar excepciones no controladas. Los servicios son muy similares, con dos diferencias principales:

  1. Se admite el registro de varios registradores de excepciones, pero solo un controlador de excepciones.
  2. Siempre se llama a los registradores de excepciones, incluso si estamos a punto de anular la conexión. Solo se llama a los controladores de excepciones cuando todavía podemos elegir qué mensaje de respuesta se va a enviar.

Ambos servicios proporcionan acceso a un contexto de excepción que contiene información relevante desde el punto en el que se detectó la excepción, especialmente la HttpRequestMessage, el HttpRequestContext, la excepción iniciada y el origen de la excepción (detalles a continuación).

Principios de diseño

  1. No hay cambios importantes Dado que esta funcionalidad se agrega en una versión secundaria, una restricción importante que afecta a la solución es que no hay cambios importantes, ya sea para escribir contratos o para el comportamiento. Esta restricción descartó alguna limpieza que nos gustaría haber realizado en términos de bloques catch existentes convirtiendo excepciones en 500 respuestas. Esta limpieza adicional es algo que podríamos considerar para una versión principal posterior.
  2. Mantener la coherencia con construcciones de API web canalización de filtro de la API web es una excelente manera de controlar los problemas transversales con la flexibilidad de aplicar la lógica en un ámbito global o específico de la acción específico del controlador. Los filtros, incluidos los filtros de excepción, siempre tienen contextos de acción y controlador, incluso cuando se registran en el ámbito global. Ese contrato tiene sentido para los filtros, pero significa que los filtros de excepción, incluso los de ámbito global, no son una buena opción para algunos casos de control de excepciones, como excepciones de controladores de mensajes, donde no existe ningún contexto de acción o controlador. Si queremos usar el ámbito flexible que ofrecen los filtros para el control de excepciones, todavía necesitamos filtros de excepciones. Pero si necesitamos controlar la excepción fuera de un contexto de controlador, también necesitamos una construcción independiente para el control de errores global completo (algo sin restricciones de contexto de controlador y contexto de acción).

Cuándo usar

  • Los registradores de excepciones son la solución para ver todas las excepciones no controladas detectadas por la API web.
  • Los controladores de excepciones son la solución para personalizar todas las respuestas posibles a excepciones no controladas detectadas por la API web.
  • Los filtros de excepciones son la solución más sencilla para procesar las excepciones no controladas del subconjunto relacionadas con una acción o controlador específicos.

Detalles del servicio

Las interfaces del servicio de controlador y registrador de excepciones son métodos asincrónicos simples que toman los contextos respectivos:

public interface IExceptionLogger
{
   Task LogAsync(ExceptionLoggerContext context, 
                 CancellationToken cancellationToken);
}

public interface IExceptionHandler
{
   Task HandleAsync(ExceptionHandlerContext context, 
                    CancellationToken cancellationToken);
}

También proporcionamos clases base para ambas interfaces. Invalidar los métodos principales (sincronización o asincrónico) es todo lo necesario para registrar o controlar en los momentos recomendados. Para el registro, la clase base de ExceptionLogger se asegurará de que solo se llame al método de registro principal una vez para cada excepción (incluso si posteriormente propaga más arriba la pila de llamadas y se detecta de nuevo). La clase base ExceptionHandler llamará al método de control principal solo para excepciones en la parte superior de la pila de llamadas, omitiendo los bloques catch anidados heredados. (Las versiones simplificadas de estas clases base se encuentran en el apéndice siguiente.) Tanto IExceptionLogger y IExceptionHandler reciben información sobre la excepción a través de un ExceptionContext.

public class ExceptionContext
{
   public Exception Exception { get; set; }

   public HttpRequestMessage Request { get; set; }

   public HttpRequestContext RequestContext { get; set; }

   public HttpControllerContext ControllerContext { get; set; }

   public HttpActionContext ActionContext { get; set; }

   public HttpResponseMessage Response { get; set; }

   public string CatchBlock { get; set; }

   public bool IsTopLevelCatchBlock { get; set; }
}

Cuando el marco llama a un registrador de excepciones o un controlador de excepciones, siempre proporcionará un Exception y un Request. Excepto para las pruebas unitarias, siempre proporcionará una RequestContext. Rara vez proporcionará ControllerContext y ActionContext (solo cuando se llama desde el bloque catch para los filtros de excepción). Rara vez proporcionará ( Responsesolo en determinados casos de IIS cuando en medio de intentar escribir la respuesta). Tenga en cuenta que, dado que algunas de estas propiedades pueden ser null el consumidor debe comprobar si hay null antes de acceder a los miembros de la clase de excepción.CatchBlock es una cadena que indica qué bloque catch vio la excepción. Las cadenas de bloque catch son las siguientes:

  • HttpServer (método SendAsync)

  • HttpControllerDispatcher (método SendAsync)

  • HttpBatchHandler (método SendAsync)

  • IExceptionFilter (procesamiento de ApiController de la canalización de filtro de excepciones en ExecuteAsync)

  • Host OWIN:

    • HttpMessageHandlerAdapter.BufferResponseContentAsync (para la salida de almacenamiento en búfer)
    • HttpMessageHandlerAdapter.CopyResponseContentAsync (para la salida de streaming)
  • Host web:

    • HttpControllerHandler.WriteBufferedResponseContentAsync (para la salida de almacenamiento en búfer)
    • HttpControllerHandler.WriteStreamedResponseContentAsync (para la salida de streaming)
    • HttpControllerHandler.WriteErrorResponseContentAsync (para errores en la recuperación de errores en modo de salida almacenado en búfer)

La lista de cadenas de bloque catch también está disponible a través de propiedades estáticas de solo lectura. (La cadena de bloque catch principal se encuentra en ExceptionCatchBlocks estáticos; el resto aparece en una clase estática cada una para OWIN y host web).IsTopLevelCatchBlock resulta útil para seguir el patrón recomendado de control de excepciones solo en la parte superior de la pila de llamadas. En lugar de convertir excepciones en 500 respuestas en cualquier lugar en que se produzca un bloque catch anidado, un controlador de excepciones puede permitir que las excepciones se propaguen hasta que el host las vea.

Además de la ExceptionContext, un registrador obtiene una información más a través de la completaExceptionLoggerContext:

public class ExceptionLoggerContext
{
   public ExceptionContext ExceptionContext { get; set; }
   public bool CanBeHandled { get; set; }
}

La segunda propiedad, CanBeHandled, permite a un registrador identificar una excepción que no se puede controlar. Cuando la conexión está a punto de anularse y no se puede enviar ningún mensaje de respuesta nuevo, se llamará a los registradores, pero el controlador no se llamará y los registradores pueden identificar este escenario de esta propiedad.

Además de la ExceptionContext, un controlador obtiene una propiedad más que puede establecer en el ExceptionHandlerContext completo para controlar la excepción:

public class ExceptionHandlerContext
{
   public ExceptionContext ExceptionContext { get; set; }
   public IHttpActionResult Result { get; set; }
}

Un controlador de excepciones indica que ha controlado una excepción estableciendo la propiedad Result en un resultado de acción (por ejemplo, un ExceptionResult, InternalServerErrorResult, StatusCodeResult, o un resultado personalizado). Si la propiedad Result es null, la excepción no está controlada y se volverá a producir la excepción original.

En el caso de las excepciones en la parte superior de la pila de llamadas, hemos realizado un paso adicional para asegurarnos de que la respuesta es adecuada para los autores de llamadas API. Si la excepción se propaga hasta el host, el autor de la llamada vería la pantalla amarilla de muerte o alguna otra respuesta proporcionada por el host que normalmente es HTML y no suele ser una respuesta de error de API adecuada. En estos casos, el resultado inicia un valor distinto de NULL y solo si un controlador de excepciones personalizado lo vuelve a establecer explícitamente en null (no controlado) se propagará la excepción al host. Establecer Result a null en estos casos puede ser útil para dos escenarios:

  1. OWIN hosted Web API con middleware de control de excepciones personalizado registrado antes o fuera de la API web.
  2. La depuración local a través de un explorador, donde la pantalla amarilla de muerte es realmente una respuesta útil para una excepción no controlada.

En el caso de los registradores de excepciones y los controladores de excepciones, no hacemos nada para recuperar si el registrador o el propio controlador inician una excepción. (Aparte de permitir que la excepción se propague, deje comentarios en la parte inferior de esta página si tiene un enfoque mejor.) El contrato para registradores y controladores de excepciones es que no deben permitir que las excepciones se propaguen a sus autores de llamada; de lo contrario, la excepción solo se propagará, a menudo hasta el host, lo que da como resultado un error HTML (como ASP. Pantalla amarilla de NET) que se devuelve al cliente (que normalmente no es la opción preferida para los autores de llamadas API que esperan JSON o XML).

Ejemplos

Registrador de excepciones de seguimiento

El registrador de excepciones siguiente envía los datos de excepción a los orígenes de seguimiento configurados (incluida la ventana de salida de depuración de Visual Studio).

class TraceExceptionLogger : ExceptionLogger
{
    public override void LogCore(ExceptionLoggerContext context)
    {
        Trace.TraceError(context.ExceptionContext.Exception.ToString());
    }
}

Controlador de excepciones de mensaje de error personalizado

El controlador de excepciones siguiente genera una respuesta de error personalizada a los clientes, incluida una dirección de correo electrónico para ponerse en contacto con el soporte técnico.

class OopsExceptionHandler : ExceptionHandler
{
    public override void HandleCore(ExceptionHandlerContext context)
    {
        context.Result = new TextPlainErrorResult
        {
            Request = context.ExceptionContext.Request,
            Content = "Oops! Sorry! Something went wrong." +
                      "Please contact support@contoso.com so we can try to fix it."
        };
    }

    private class TextPlainErrorResult : IHttpActionResult
    {
        public HttpRequestMessage Request { get; set; }

        public string Content { get; set; }

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            HttpResponseMessage response = 
                             new HttpResponseMessage(HttpStatusCode.InternalServerError);
            response.Content = new StringContent(Content);
            response.RequestMessage = Request;
            return Task.FromResult(response);
        }
    }
}

Registro de filtros de excepciones

Si usa la plantilla de proyecto "ASP.NET aplicación web MVC 4" para crear el proyecto, coloque el código de configuración de la API web dentro de la clase WebApiConfig, en la carpeta App_Start:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute());

        // Other configuration code...
    }
}

Apéndice: Detalles de la clase base

public class ExceptionLogger : IExceptionLogger
{
    public virtual Task LogAsync(ExceptionLoggerContext context, 
                                 CancellationToken cancellationToken)
    {
        if (!ShouldLog(context))
        {
            return Task.FromResult(0);
        }

        return LogAsyncCore(context, cancellationToken);
    }

    public virtual Task LogAsyncCore(ExceptionLoggerContext context, 
                                     CancellationToken cancellationToken)
    {
        LogCore(context);
        return Task.FromResult(0);
    }

    public virtual void LogCore(ExceptionLoggerContext context)
    {
    }

    public virtual bool ShouldLog(ExceptionLoggerContext context)
    {
        IDictionary exceptionData = context.ExceptionContext.Exception.Data;

        if (!exceptionData.Contains("MS_LoggedBy"))
        {
            exceptionData.Add("MS_LoggedBy", new List<object>());
        }

        ICollection<object> loggedBy = ((ICollection<object>)exceptionData[LoggedByKey]);

        if (!loggedBy.Contains(this))
        {
            loggedBy.Add(this);
            return true;
        }
        else
        {
            return false;
        }
    }
}

public class ExceptionHandler : IExceptionHandler
{
    public virtual Task HandleAsync(ExceptionHandlerContext context, 
                                    CancellationToken cancellationToken)
    {
        if (!ShouldHandle(context))
        {
            return Task.FromResult(0);
        }

        return HandleAsyncCore(context, cancellationToken);
    }

    public virtual Task HandleAsyncCore(ExceptionHandlerContext context, 
                                       CancellationToken cancellationToken)
    {
        HandleCore(context);
        return Task.FromResult(0);
    }

    public virtual void HandleCore(ExceptionHandlerContext context)
    {
    }

    public virtual bool ShouldHandle(ExceptionHandlerContext context)
    {
        return context.ExceptionContext.IsOutermostCatchBlock;
    }
}