try practice net httpresponseexception handling exceptions exceptionfilterattribute error catch best asp all c# exception-handling asp.net-web-api http-status-codes

c# - practice - ¿Lanzar HttpResponseException o devolver Request.CreateErrorResponse?



web api exception handling best practice (9)

Después de revisar un artículo Exception Handling en ASP.NET Web API , estoy un poco confundido en cuanto a cuándo lanzar una excepción vs devolver una respuesta de error. También me pregunto si es posible modificar la respuesta cuando su método devuelve un modelo específico de dominio en lugar de HttpResponseMessage ...

Entonces, para recapitular aquí están mis preguntas seguidas por algún código con mayúsculas y minúsculas:

Preguntas

Preguntas sobre el Caso # 1

  1. ¿Debo usar siempre HttpResponseMessage lugar de un modelo de dominio concreto, para que el mensaje se pueda personalizar?
  2. ¿Se puede personalizar el mensaje si devuelve un modelo de dominio concreto?

Preguntas con respecto al caso # 2,3,4

  1. ¿Debo lanzar una excepción o devolver una respuesta de error? Si la respuesta es "depende", ¿puede dar situaciones / ejemplos sobre cuándo usar uno frente al otro?
  2. ¿Cuál es la diferencia entre lanzar HttpResponseException vs Request.CreateErrorResponse ? La salida al cliente parece idéntica ...
  3. ¿Debo usar siempre HttpError para "envolver" los mensajes de respuesta en errores (ya sea que se HttpError la excepción o se devuelva la respuesta de error)?

Ejemplos de casos

// CASE #1 public Customer Get(string id) { var customer = _customerService.GetById(id); if (customer == null) { var notFoundResponse = new HttpResponseMessage(HttpStatusCode.NotFound); throw new HttpResponseException(notFoundResponse); } //var response = Request.CreateResponse(HttpStatusCode.OK, customer); //response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300)); return customer; } // CASE #2 public HttpResponseMessage Get(string id) { var customer = _customerService.GetById(id); if (customer == null) { var notFoundResponse = new HttpResponseMessage(HttpStatusCode.NotFound); throw new HttpResponseException(notFoundResponse); } var response = Request.CreateResponse(HttpStatusCode.OK, customer); response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300)); return response; } // CASE #3 public HttpResponseMessage Get(string id) { var customer = _customerService.GetById(id); if (customer == null) { var message = String.Format("customer with id: {0} was not found", id); var errorResponse = Request.CreateErrorResponse(HttpStatusCode.NotFound, message); throw new HttpResponseException(errorResponse); } var response = Request.CreateResponse(HttpStatusCode.OK, customer); response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300)); return response; } // CASE #4 public HttpResponseMessage Get(string id) { var customer = _customerService.GetById(id); if (customer == null) { var message = String.Format("customer with id: {0} was not found", id); var httpError = new HttpError(message); return Request.CreateErrorResponse(HttpStatusCode.NotFound, httpError); } var response = Request.CreateResponse(HttpStatusCode.OK, customer); response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300)); return response; }

Actualizar

Para ayudar a demostrar aún más los casos # 2,3,4, el siguiente fragmento de código resalta varias opciones que "pueden suceder" cuando no se encuentra un cliente ...

if (customer == null) { // which of these 4 options is the best strategy for Web API? // option 1 (throw) var notFoundMessage = new HttpResponseMessage(HttpStatusCode.NotFound); throw new HttpResponseException(notFoundMessage); // option 2 (throw w/ HttpError) var message = String.Format("Customer with id: {0} was not found", id); var httpError = new HttpError(message); var errorResponse = Request.CreateErrorResponse(HttpStatusCode.NotFound, httpError); throw new HttpResponseException(errorResponse); // option 3 (return) var message = String.Format("Customer with id: {0} was not found", id); return Request.CreateErrorResponse(HttpStatusCode.NotFound, message); // option 4 (return w/ HttpError) var message = String.Format("Customer with id: {0} was not found", id); var httpError = new HttpError(message); return Request.CreateErrorResponse(HttpStatusCode.NotFound, httpError); }


Caso 1

  1. No necesariamente, hay otros lugares en la tubería para modificar la respuesta (filtros de acción, manejadores de mensajes).
  2. Vea arriba, pero si la acción devuelve un modelo de dominio, entonces no puede modificar la respuesta dentro de la acción.

Casos # 2-4

  1. Las principales razones para lanzar HttpResponseException son:
    • si devuelve un modelo de dominio pero necesita manejar casos de error,
    • para simplificar la lógica de su controlador tratando los errores como excepciones
  2. Estos deberían ser equivalentes; HttpResponseException encapsula un HttpResponseMessage, que es lo que se devuelve como la respuesta HTTP.

    por ejemplo, el caso n. ° 2 podría reescribirse como

    public HttpResponseMessage Get(string id) { HttpResponseMessage response; var customer = _customerService.GetById(id); if (customer == null) { response = new HttpResponseMessage(HttpStatusCode.NotFound); } else { response = Request.CreateResponse(HttpStatusCode.OK, customer); response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300)); } return response; }

    ... pero si la lógica de su controlador es más complicada, lanzar una excepción podría simplificar el flujo de código.

  3. HttpError le proporciona un formato coherente para el cuerpo de la respuesta y se puede serializar a JSON / XML / etc., pero no es obligatorio. por ejemplo, es posible que no desee incluir un cuerpo de entidad en la respuesta, o puede que desee algún otro formato.


El enfoque que he tomado es simplemente lanzar excepciones de las acciones del controlador de API y tener un filtro de excepción registrado que procese la excepción y establezca una respuesta apropiada en el contexto de ejecución de la acción.

El filtro expone una interfaz fluida que proporciona un medio para registrar manejadores para tipos específicos de excepciones antes de registrar el filtro con la configuración global.

El uso de este filtro permite el manejo centralizado de excepciones en lugar de distribuirlo entre las acciones del controlador. Sin embargo, hay casos en los que detectaré excepciones dentro de la acción del controlador y devolveré una respuesta específica si no tiene sentido centralizar el manejo de esa excepción en particular.

Ejemplo de registro de filtro:

GlobalConfiguration.Configuration.Filters.Add( new UnhandledExceptionFilterAttribute() .Register<KeyNotFoundException>(HttpStatusCode.NotFound) .Register<SecurityException>(HttpStatusCode.Forbidden) .Register<SqlException>( (exception, request) => { var sqlException = exception as SqlException; if (sqlException.Number > 50000) { var response = request.CreateResponse(HttpStatusCode.BadRequest); response.ReasonPhrase = sqlException.Message.Replace(Environment.NewLine, String.Empty); return response; } else { return request.CreateResponse(HttpStatusCode.InternalServerError); } } ) );

Clase UnhandledExceptionFilterAttribute:

using System; using System.Collections.Concurrent; using System.Net; using System.Net.Http; using System.Text; using System.Web.Http.Filters; namespace Sample { /// <summary> /// Represents the an attribute that provides a filter for unhandled exceptions. /// </summary> public class UnhandledExceptionFilterAttribute : ExceptionFilterAttribute { #region UnhandledExceptionFilterAttribute() /// <summary> /// Initializes a new instance of the <see cref="UnhandledExceptionFilterAttribute"/> class. /// </summary> public UnhandledExceptionFilterAttribute() : base() { } #endregion #region DefaultHandler /// <summary> /// Gets a delegate method that returns an <see cref="HttpResponseMessage"/> /// that describes the supplied exception. /// </summary> /// <value> /// A <see cref="Func{Exception, HttpRequestMessage, HttpResponseMessage}"/> delegate method that returns /// an <see cref="HttpResponseMessage"/> that describes the supplied exception. /// </value> private static Func<Exception, HttpRequestMessage, HttpResponseMessage> DefaultHandler = (exception, request) => { if(exception == null) { return null; } var response = request.CreateResponse<string>( HttpStatusCode.InternalServerError, GetContentOf(exception) ); response.ReasonPhrase = exception.Message.Replace(Environment.NewLine, String.Empty); return response; }; #endregion #region GetContentOf /// <summary> /// Gets a delegate method that extracts information from the specified exception. /// </summary> /// <value> /// A <see cref="Func{Exception, String}"/> delegate method that extracts information /// from the specified exception. /// </value> private static Func<Exception, string> GetContentOf = (exception) => { if (exception == null) { return String.Empty; } var result = new StringBuilder(); result.AppendLine(exception.Message); result.AppendLine(); Exception innerException = exception.InnerException; while (innerException != null) { result.AppendLine(innerException.Message); result.AppendLine(); innerException = innerException.InnerException; } #if DEBUG result.AppendLine(exception.StackTrace); #endif return result.ToString(); }; #endregion #region Handlers /// <summary> /// Gets the exception handlers registered with this filter. /// </summary> /// <value> /// A <see cref="ConcurrentDictionary{Type, Tuple}"/> collection that contains /// the exception handlers registered with this filter. /// </value> protected ConcurrentDictionary<Type, Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>> Handlers { get { return _filterHandlers; } } private readonly ConcurrentDictionary<Type, Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>> _filterHandlers = new ConcurrentDictionary<Type, Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>>(); #endregion #region OnException(HttpActionExecutedContext actionExecutedContext) /// <summary> /// Raises the exception event. /// </summary> /// <param name="actionExecutedContext">The context for the action.</param> public override void OnException(HttpActionExecutedContext actionExecutedContext) { if(actionExecutedContext == null || actionExecutedContext.Exception == null) { return; } var type = actionExecutedContext.Exception.GetType(); Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> registration = null; if (this.Handlers.TryGetValue(type, out registration)) { var statusCode = registration.Item1; var handler = registration.Item2; var response = handler( actionExecutedContext.Exception.GetBaseException(), actionExecutedContext.Request ); // Use registered status code if available if (statusCode.HasValue) { response.StatusCode = statusCode.Value; } actionExecutedContext.Response = response; } else { // If no exception handler registered for the exception type, fallback to default handler actionExecutedContext.Response = DefaultHandler( actionExecutedContext.Exception.GetBaseException(), actionExecutedContext.Request ); } } #endregion #region Register<TException>(HttpStatusCode statusCode) /// <summary> /// Registers an exception handler that returns the specified status code for exceptions of type <typeparamref name="TException"/>. /// </summary> /// <typeparam name="TException">The type of exception to register a handler for.</typeparam> /// <param name="statusCode">The HTTP status code to return for exceptions of type <typeparamref name="TException"/>.</param> /// <returns> /// This <see cref="UnhandledExceptionFilterAttribute"/> after the exception handler has been added. /// </returns> public UnhandledExceptionFilterAttribute Register<TException>(HttpStatusCode statusCode) where TException : Exception { var type = typeof(TException); var item = new Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>( statusCode, DefaultHandler ); if (!this.Handlers.TryAdd(type, item)) { Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> oldItem = null; if (this.Handlers.TryRemove(type, out oldItem)) { this.Handlers.TryAdd(type, item); } } return this; } #endregion #region Register<TException>(Func<Exception, HttpRequestMessage, HttpResponseMessage> handler) /// <summary> /// Registers the specified exception <paramref name="handler"/> for exceptions of type <typeparamref name="TException"/>. /// </summary> /// <typeparam name="TException">The type of exception to register the <paramref name="handler"/> for.</typeparam> /// <param name="handler">The exception handler responsible for exceptions of type <typeparamref name="TException"/>.</param> /// <returns> /// This <see cref="UnhandledExceptionFilterAttribute"/> after the exception <paramref name="handler"/> /// has been added. /// </returns> /// <exception cref="ArgumentNullException">The <paramref name="handler"/> is <see langword="null"/>.</exception> public UnhandledExceptionFilterAttribute Register<TException>(Func<Exception, HttpRequestMessage, HttpResponseMessage> handler) where TException : Exception { if(handler == null) { throw new ArgumentNullException("handler"); } var type = typeof(TException); var item = new Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>( null, handler ); if (!this.Handlers.TryAdd(type, item)) { Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> oldItem = null; if (this.Handlers.TryRemove(type, out oldItem)) { this.Handlers.TryAdd(type, item); } } return this; } #endregion #region Unregister<TException>() /// <summary> /// Unregisters the exception handler for exceptions of type <typeparamref name="TException"/>. /// </summary> /// <typeparam name="TException">The type of exception to unregister handlers for.</typeparam> /// <returns> /// This <see cref="UnhandledExceptionFilterAttribute"/> after the exception handler /// for exceptions of type <typeparamref name="TException"/> has been removed. /// </returns> public UnhandledExceptionFilterAttribute Unregister<TException>() where TException : Exception { Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> item = null; this.Handlers.TryRemove(typeof(TException), out item); return this; } #endregion } }

El código fuente también se puede encontrar here .


En situaciones de error, quería devolver una clase de detalles de error específica, en cualquier formato solicitado por el cliente en lugar del objeto de ruta feliz.

Quiero que mis métodos de controlador devuelvan el objeto de ruta feliz de dominio específico y arrojar una excepción de lo contrario.

El problema que tuve fue que los constructores HttpResponseException no permiten objetos de dominio.

Esto es lo que finalmente se me ocurrió

public ProviderCollection GetProviders(string providerName) { try { return _providerPresenter.GetProviders(providerName); } catch (BadInputValidationException badInputValidationException) { throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest, badInputValidationException.Result)); } }

Result es una clase que contiene detalles de error, mientras que ProviderCollection es mi resultado de ruta feliz.


Me gusta la respuesta de oposición

De todos modos, necesitaba una forma de atrapar la Excepción heredada y esa solución no satisface todas mis necesidades.

Así que terminé cambiando la forma en que maneja OnException y esta es mi versión

public override void OnException(HttpActionExecutedContext actionExecutedContext) { if (actionExecutedContext == null || actionExecutedContext.Exception == null) { return; } var type = actionExecutedContext.Exception.GetType(); Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> registration = null; if (!this.Handlers.TryGetValue(type, out registration)) { //tento di vedere se ho registrato qualche eccezione che eredita dal tipo di eccezione sollevata (in ordine di registrazione) foreach (var item in this.Handlers.Keys) { if (type.IsSubclassOf(item)) { registration = this.Handlers[item]; break; } } } //se ho trovato un tipo compatibile, uso la sua gestione if (registration != null) { var statusCode = registration.Item1; var handler = registration.Item2; var response = handler( actionExecutedContext.Exception.GetBaseException(), actionExecutedContext.Request ); // Use registered status code if available if (statusCode.HasValue) { response.StatusCode = statusCode.Value; } actionExecutedContext.Response = response; } else { // If no exception handler registered for the exception type, fallback to default handler actionExecutedContext.Response = DefaultHandler(actionExecutedContext.Exception.GetBaseException(), actionExecutedContext.Request ); } }

El núcleo es este ciclo donde verifico si el tipo de excepción es una subclase de un tipo registrado.

foreach (var item in this.Handlers.Keys) { if (type.IsSubclassOf(item)) { registration = this.Handlers[item]; break; } }

my2cents


Otro caso de cuándo usar HttpResponseException lugar de Response.CreateResponse(HttpStatusCode.NotFound) u otro código de estado de error es si tiene transacciones en filtros de acción y desea que las transacciones se retrotraigan cuando se devuelve una respuesta de error al cliente.

El uso de Response.CreateResponse no retrotraerá la transacción, mientras que lanzar una excepción sí lo hará.


Por lo que puedo decir, si lanza una excepción o devuelve Request.CreateErrorResponse, el resultado es el mismo. Si nos fijamos en el código fuente de System.Web.Http.dll, verá tanto. Eche un vistazo a este resumen general, y una solución muy similar que he hecho: Web Api, HttpError y el comportamiento de las excepciones


Si no devuelve HttpResponseMessage y en su lugar devuelve clases de entidad / modelo directamente, un enfoque que he encontrado útil es agregar la siguiente función de utilidad a mi controlador

private void ThrowResponseException(HttpStatusCode statusCode, string message) { var errorResponse = Request.CreateErrorResponse(statusCode, message); throw new HttpResponseException(errorResponse); }

y simplemente llámalo con el código de estado y el mensaje apropiados


Quiero señalar que, según mi experiencia, si se lanza una HttpResponseException en lugar de devolver un HttpResponseMessage en un método webapi 2, si se realiza una llamada inmediatamente a IIS Express, se agotó el tiempo de espera o se devuelve un 200 pero con un error html en la respuesta. La forma más fácil de probar esto es hacer una llamada $ .ajax a un método que arroje una HttpResponseException y en el error CallBack en ajax haga una llamada inmediata a otro método o incluso a una simple página http. Notarás que la llamada de imediate fallará. Si agrega un punto de interrupción o un settimeout () en la llamada de error, vuelva a retrasar la segunda llamada por un segundo o dos, lo que le dará tiempo al servidor para recuperarla y funcionar correctamente. Esto no lo hace, pero es casi como el lanzamiento HttpResponseException hace que el subproceso de escucha del lado del servidor salga y se reinicie causando una fracción de segundo de que ningún servidor acepte conexiones o algo así.

Actualización: La causa raíz de la extraña conexión Ajax Timeout es que si una llamada ajax se hace lo suficientemente rápido se utiliza la misma conexión tcp. Estaba generando un error de 401 éter al devolver un HttpResonseMessage o lanzar una HTTPResponseException que se devolvió a la llamada ajax del navegador. Pero junto con esa llamada, MS estaba devolviendo un Objeto No Encontrado Error porque en el Inicio .Auth.vb app.UserCookieAuthentication estaba habilitada por lo que estaba tratando de volver a interceptar la respuesta y agregar una redirección pero tenía un error con Object not Instance of a Object. Este error era html pero se adjuntó a la respuesta después del hecho, por lo que solo si la llamada ajax se hizo lo suficientemente rápido y se usó la misma conexión tcp, se devolvió al navegador y luego se adjuntó al frente de la siguiente llamada. Por alguna razón, Chrome acaba el tiempo de espera, el fiddler se rompió debido a la mezcla de json y htm, pero Firefox provocó el error real. Tan extraño, pero sniffer de paquetes o Firefox era la única manera de rastrear este.

También debe tenerse en cuenta que si utiliza la ayuda de la API web para generar ayuda automática y devuelve HttpResponseMessage, debe agregar un

[System.Web.Http.Description.ResponseType(typeof(CustomReturnedType))]

atribuir al método para que la ayuda se genere correctamente. Entonces

return Request.CreateResponse<CustomReturnedType>(objCustomeReturnedType)

o en error

return Request.CreateErrorResponse( System.Net.HttpStatusCode.InternalServerError, new Exception("An Error Ocurred"));

Espero que esto ayude a otra persona que puede estar obteniendo un tiempo de espera aleatorio o servidor no disponible inmediatamente después de lanzar una HttpResponseException.

Además, devolver una HttpResponseException tiene el beneficio adicional de no causar que Visual Studio rompa una excepción no gestionada útil cuando el error que se devuelve es que AuthToken necesita actualizarse en una sola aplicación de página.

Actualización: me estoy retractando de mi declaración sobre el tiempo de espera de IIS Express, esto fue un error en mi cliente llamada ajax, resulta que Ajax 1.8 devolvió $ .ajax () y devolvió $ .ajax. (). Then () ambos devuelven la promesa pero no la misma promesa encadenada, entonces () devuelve una nueva promesa que hizo que el orden de ejecución fuera incorrecto. Entonces cuando la promesa de entonces () se completó fue un tiempo de espera de script. Were extraño pero no un problema expreso de IIS un problema entre el teclado y la silla.


No arroje una HttpResponseException ni devuelva un HttpResponesMessage para errores, excepto si la intención es finalizar la solicitud con ese resultado exacto .

Las excepciones HttpResponseException no se manejan igual que otras excepciones . No están atrapados en los filtros de excepción . No están atrapados en el controlador de excepciones . Son una forma astuta de deslizar un HttpResponseMessage mientras termina el flujo de ejecución del código actual.

¡A menos que el código sea un código de infraestructura que dependa de esta eliminación especial, evite usar el tipo HttpResponseException!

HttpResponseMessage no son excepciones. No terminan el flujo de ejecución del código actual. No se pueden filtrar como excepciones. No se pueden registrar como excepciones. Representan un resultado válido: ¡incluso una respuesta de 500 es "una respuesta válida sin excepción"!

Haz la vida más simple:

Cuando hay un caso de error / excepcional, ejecute una excepción normal de .NET o un tipo de excepción de aplicación personalizada (que no se derive de HttpResponseException) con las propiedades de "error / respuesta HTTP" deseadas, como un código de estado, según la excepción normal manejo

Use los filtros de excepción / manejadores de excepciones / registradores de excepciones para hacer algo apropiado con estos casos excepcionales: cambiar / agregar códigos de estado? agregar identificadores de seguimiento? incluir rastros de pila? ¿Iniciar sesión?

Al evitar HttpResponseException, el manejo del ''caso excepcional'' se hace uniforme y se puede manejar como parte de la tubería expuesta. Por ejemplo, uno puede convertir un ''NotFound'' en un 404 y un ''ArgumentException'' en un 400 y un ''NullReference'' en un 500 de forma fácil y uniforme con excepciones de nivel de aplicación, mientras permite que la extensibilidad brinde "aspectos básicos" como el registro de errores.