Manejo de excepciones de ASP.NET con IRO.Mvc.MvcExceptionHandler



Si usted es un desarrollador de backend de C #, probablemente tarde o temprano necesite encontrar una forma unificada de manejar situaciones excepcionales. Aunque, incluso si está contento con el código 500 en la respuesta, este artículo aún ayudará a mejorar su método, sin forzar nada a reescribir.

Hablaremos sobre la biblioteca ASP.NET, que le permite resolver este problema de la manera más elegante posible. Para aquellos que son demasiado vagos para leer un artículo largo: el archivo Léame y la biblioteca están aquí , un ejemplo está aquí . Disponible en nuget.org y solo me alegraré si beneficiará a alguien. Y así, pasemos al código. Primero, echemos un vistazo a las alternativas.

Una de las primeras cosas que pueden venir a la mente es crear un DTO (objeto de transferencia de datos) para manejar excepciones, capturar una excepción en el controlador (aunque no es necesario que sea una excepción, es posible verificar si es nulo o algo así), Complete los datos en el DTO y envíelos al cliente. El código para este método podría verse así:

public IActionResult Get() { try { //Code with exception. } catch (Exception ex) { return new JsonResult( new ErrorDto { IsError = true, Message = ex.Message }); } } 

Otra opción es usar códigos de estado HTTP para esto.

 public IActionResult Get() { try { //Code with exception. } catch (Exception ex) { return BadRequest(); } } 

Una práctica bastante común, pero con sus inconvenientes: puede ser difícil describir la esencia de su situación con uno de los códigos estándar, por lo que el mismo código puede interpretarse de manera diferente incluso en el mismo sistema, y ​​también proporciona muy poca información para la depuración al desarrollador.

Y aquí algunos incluso pueden comenzar a combinar ambos métodos, y en diferentes proporciones. En algún lugar se olvidarán de enviar el DTO, en algún lugar no se enviará el código o se enviará el incorrecto, pero en algún lugar en general se serializará con la configuración json incorrecta y no devolverá lo que se necesita.

Frente a lo anterior, muchos están tratando de resolver este problema usando app.UseExceptionHandler (); manejando excepciones a través de él. Este es un buen intento, pero no le permitirá olvidarse fácilmente del problema. Primero, aún enfrentará el problema de elegir un DTO para excepciones. En segundo lugar, dicho controlador no permitirá procesar códigos de error http que fueron devueltos por los controladores, porque No se produjo una excepción. En tercer lugar, de esta manera es inconveniente resolver el problema de clasificación de errores, tendrá que escribir mucho código para adjuntar un mensaje, código http o algo a cada excepción. Y en cuarto lugar, pierde la oportunidad de utilizar AspA DeveloperExceptionPage, lo cual es muy inconveniente para la depuración. Incluso si de alguna manera resuelve este problema, todos los desarrolladores de este proyecto tendrán que seguir estrictamente las especificaciones, construir el manejo de errores específicamente en las excepciones, no devolver sus DTO, de lo contrario, los errores en su API pueden verse diferentes de un método a otro.

Opción de manejo de excepción seleccionada


Antes de mostrar cómo IRO.Mvc.MvcExceptionHandler le permite manejar excepciones, primero describiré cómo veo el manejo ideal de excepciones. Para hacer esto, establecemos una serie de requisitos:

  1. Debería ser un DTO, pero tampoco rechazamos los códigos http, porque para muchos errores, todavía son adecuados, se pueden usar en todas partes y en un proyecto antiguo que también debe admitir, y son simplemente universales. El DTO estándar incluirá el campo IsError (que permite escribir el manejo universal de errores en el cliente), también debe contener el código de error de cadena ErrorKey, que el desarrollador puede reconocer inmediatamente solo al mirarlo y que proporciona más información. Además, puede agregar un enlace a una página con una descripción de este error, si es necesario.
  2. Todo esto está en la producción. En modo de desarrollo, este DTO debe devolver un seguimiento de la pila, solicitar datos: cookies, encabezados, parámetros. Mirando hacia el futuro, el middleware descrito en el artículo incluso devuelve un enlace a la DeveloperExceptionPage generada, que le permite ver la excepción de excepción en una forma conveniente, pero más sobre eso más adelante.
  3. El desarrollador puede enlazar la excepción, el código de error http y ErrorKey. Esto significa que si envía el código 403 desde el controlador, entonces si el desarrollador le adjuntó una ErrorKey específica, se devolverá el DTO con él. Y viceversa, si se produce una NoccessAccessException no autorizada, estará vinculada al código http y ErrorKey.

Este es el formato predeterminado utilizado en la biblioteca:

 { "__IsError": true, "ErrorKey": "ClientException", "InfoUrl": "https://iro.com/errors/ClientException" } 

Debo decir de inmediato que la forma en que se transmitirán los datos al cliente puede ser absolutamente cualquiera, esta es solo una de las opciones.

IRO.Mvc.MvcExceptionHandler


Ahora mostraré cómo resolví este problema por mí mismo escribiendo la biblioteca IRO.Mvc.MvcExceptionHandler.

Conectamos un controlador de excepciones como cualquier otro middleware, en la clase de inicio.

 app.UseMvcExceptionHandler((s) => { //Settings... }); 

Dentro del delegado que se delega, necesitamos configurar nuestro middleware. Es necesario asignar (vincular) excepciones a códigos http y ErrorKey. A continuación se muestra la opción de configuración más fácil.

  s.Mapping((builder) => { builder.RegisterAllAssignable<Exception>( httpCode: 500, errorKeyPrefix: "Ex_" ); }); 

Como prometí a los desarrolladores hardcore más perezosos que no están acostumbrados a manejar excepciones, no se necesita hacer nada más. Este código vinculará todas las excepciones en la canalización de ASP.NET al DTO común con el código 500, y el nombre de la excepción se escribirá en ErrorKey.

Vale la pena entender que el método RegisterAllAssignable no solo registra una excepción del tipo especificado, sino también todos sus descendientes. Si desea enviar solo información sobre excepciones específicas al cliente, es una decisión completamente razonable crear su ClientException y asignarla solo. Al mismo tiempo, si configura un código http para ClientException y otro para su sucesor SpecialClientException, el código SpecialClientException se utilizará para todos sus descendientes, ignorando la configuración de ClientException. Todo esto se almacena en caché, por lo que no habrá problemas de rendimiento.

Puede ajustar y registrar su ErrorKey y su código http para una excepción específica:

  s.Mapping((builder) => { //By exception, custom error key. builder.Register<ArgumentNullException>( httpCode: 555, errorKey: "CustomErrorKey" ); //By http code. builder.Register( httpCode: 403, errorKey: "Forbidden" ); //By exception, default ErrorKey and http code. builder.Register<NullReferenceException>(); //Alternative registration method. builder.Register((ErrorInfo) new ErrorInfo() { ErrorKey = "MyError", ExceptionType = typeof(NotImplementedException), HttpCode = 556 }); }); 

Además del mapeo, vale la pena configurar pesos medios. Puede especificar la configuración de serialización de json, la dirección de su sitio, un enlace a la página de descripción del error, el modo de funcionamiento del middleware a través de IsDebug, el código http estándar para excepciones no controladas.

  s.ErrorDescriptionUrlHandler = new FormattedErrorDescriptionUrlHandler("https://iro.com/errors/{0}"); s.IsDebug = isDebug; s.DefaultHttpCode = 500; s.JsonSerializerSettings.Formatting = Formatting.Indented; s.Host="https://iro.com"; s.CanBindByHttpCode = true; 

La última propiedad indica si es posible vincular nuestro DTO por el código http.
También puede especificar cómo manejar situaciones con excepciones internas, por ejemplo, TaskCanceledException con un error interno registrado debido a .Wait (). Por ejemplo, aquí hay un solucionador estándar que elimina las excepciones internas de tales excepciones y ya funciona con ellas:

  s.InnerExceptionsResolver = InnerExceptionsResolvers.InspectAggregateException; 

Si necesita ajustar la serialización, puede configurar el método FilterAfterDTO. Regrese true para deshabilitar el procesamiento estándar y serializar errorContext.ErrorDTO como desee. Hay acceso al HttpContext y el error en sí.

  s.FilterAfterDTO = async (errorContext) => { //Custom error handling. Return true if MvcExceptionHandler must ignore current error, //because it was handled. return false; }; 

DeveloperExceptionPage y otras ventajas del modo de depuración


Hemos descubierto la configuración, ahora veamos cómo depurarlo todo. En el producto DTO, la respuesta en la respuesta es simple y ya lo mostré arriba, ahora mostraré cómo se ve el mismo DTO en modo de depuración:



Como puede ver, hay mucha más información aquí, hay stackrace y datos de solicitud. Pero es aún más conveniente simplemente seguir el enlace en el campo DebugUrl y ver los datos de error sin forzar:



Fue bastante difícil implementar esta función, ya que DeveloperExceptionPage simplemente no está destinado a ser utilizado por desarrolladores externos. Inicialmente, era imposible abrir el enlace en un navegador con una sesión diferente, el contenido dejó de mostrarse después de un reinicio. Todo esto podría resolverse solo almacenando en caché la respuesta html de este middlever. Ahora puede pasar al menos el enlace de excepción a su compañero de equipo si utiliza un servidor dedicado compartido.

Conclusión


Espero que los desarrolladores que leen este artículo hayan encontrado una herramienta interesante y útil para ellos. Para mí, este artículo es en parte una prueba de la utilidad de sus desarrollos y artículos sobre ellos. Tengo algunos proyectos más geniales ya preparados que me gustaría contarle a la comunidad de Habr.

Source: https://habr.com/ru/post/469731/


All Articles