Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
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 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:
- Excepciones iniciadas por constructores del controlador.
- Excepciones iniciadas por controladores de mensajes.
- Excepciones iniciadas durante el enrutamiento.
- 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 los filtros de excepciones, los 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 cuenta con una infraestructura de seguimiento que captura las condiciones de error, esta infraestructura está destinada a fines de diagnóstico y no está diseñada ni adecuada para su uso 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).
Introducción a 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:
- Se admite el registro de varios registradores de excepciones, pero solo un controlador de excepciones.
- Siempre se invocan los registradores de excepciones, incluso si estamos a punto de interrumpir 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 HttpRequestMessage, HttpRequestContext, la excepción iniciada y el origen de la excepción (detalles a continuación).
Principios de diseño
- Sin cambios disruptivos Dado que esta funcionalidad se agrega en una versión secundaria, una restricción importante que afecte a la solución es que no haya cambios disruptivos, ya sea en los contratos de tipo o en el comportamiento. Esta restricción descartó cierta limpieza que nos habría gustado realizar en términos de bloques de captura existentes que convierten las excepciones en respuestas 500. Esta limpieza adicional es algo que podríamos considerar para una futura versión principal.
- Mantenimiento de la coherencia con construcciones de API web La canalización de filtros de la API web es una excelente manera de gestionar los problemas transversales con la flexibilidad de aplicar la lógica en un ámbito específico de la acción, específico del controlador o global. 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 básicos (sincrónicos o asincrónicos) es todo lo que se requiere para registrar o manejar en los momentos adecuados. Para el registro, la ExceptionLogger clase base garantizará que el método de registro principal solo se llame una vez por cada excepción (incluso si más adelante se propaga más arriba en la pila de llamadas y se captura de nuevo). La ExceptionHandler clase base llamará al método principal de manejo solo para excepciones en la parte superior de la pila de llamadas, ignorando los bloques catch incrustados heredados. (Las versiones simplificadas de estas clases base se encuentran en el apéndice siguiente). Tanto IExceptionLogger como IExceptionHandler reciben información sobre la excepción a través de .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 framework llama a un registrador de excepciones o a un controlador de excepciones, siempre proporcionará un Exception y un Request. Excepto para las pruebas unitarias, también proporcionará siempre un RequestContext. Rara vez proporcionará un ControllerContext y un ActionContext (solo cuando se llame desde el bloque catch para los filtros de excepción). Rara vez proporcionará Response(solo en determinados casos de IIS cuando se está en medio de intentar escribir la respuesta). Tenga en cuenta que, dado que algunas de estas propiedades pueden ser null, el consumidor debe comprobar null antes de acceder a los miembros de la clase de excepción.
CatchBlock es una cadena que indica qué bloque de captura detectó 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)
OWIN Servidor
- HttpMessageHandlerAdapter.BufferResponseContentAsync (para el almacenamiento en búfer del contenido de salida)
- HttpMessageHandlerAdapter.CopyResponseContentAsync (para la salida de streaming)
Alojamiento web:
- HttpControllerHandler.WriteBufferedResponseContentAsync (para la salida de almacenamiento en búfer)
- HttpControllerHandler.WriteStreamedResponseContentAsync (para la salida de streaming)
- HttpControllerHandler.WriteErrorResponseContentAsync (para fallos en la recuperación de errores en modo de salida 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 principal del bloque catch se encuentra en ExceptionCatchBlocks estáticos; el resto aparece en una clase estática para OWIN y otra para el 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 ExceptionContext, un registrador obtiene una parte más de información a través del completo ExceptionLoggerContext:
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 no se llamará al controlador y los registradores pueden identificar este escenario a partir de esta propiedad.
Además de ExceptionContext, un controlador obtiene una propiedad más que puede establecer en todo ExceptionHandlerContext 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 Result propiedad en un resultado de acción (por ejemplo, exceptionResult, InternalServerErrorResult, StatusCodeResult o un resultado personalizado). Si la Result propiedad es null, la excepción no está controlada y se lanzará de nuevo la excepción original.
En el caso de las excepciones en el nivel superior de la pila de llamadas, hemos realizado un paso adicional para asegurarnos de que la respuesta sea adecuada para los usuarios de la 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 comienza como no nulo, y solo si un controlador de excepciones personalizado lo vuelve a establecer explícitamente en null (no controlado), solo entonces la excepción se propagará al host. Establecer Result en null en estos casos puede ser útil para dos escenarios.
- OWIN hospedada en Web API con middleware personalizado de manejo de excepciones registrado antes de o fuera de la API web.
- 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 logger y manejadores de excepciones es que no deben permitir que las excepciones se propaguen a sus llamadores; de lo contrario, la excepción solo se propagará, a menudo hasta el host, lo que da como resultado un error HTML (como la pantalla amarilla de ASP.NET) que se devuelve al cliente (que normalmente no es la opción preferida para los llamadores de APIs que esperan JSON o XML).
Ejemplos
Registrador de excepciones de seguimiento
El siguiente registrador de excepciones envía los datos de excepción a los orígenes de seguimiento configurados (incluida la ventana de salida de depuración en Visual Studio).
class TraceExceptionLogger : ExceptionLogger
{
public override void LogCore(ExceptionLoggerContext context)
{
Trace.TraceError(context.ExceptionContext.Exception.ToString());
}
}
Manejador 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 WebApiConfig clase , 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;
}
}