Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 17 de noviembre de 2015
ASP.NET CoreSeguro que ya conocéis la respuesta: no está. Desapareció. Kaput. Es simplemente otro de los efectos colaterales derivados de los cambios en ASP.NET Core, y más concretamente, de la sustitución del archivo web.config por otros mecanismos de configuración.

Sin embargo, seguro que también estaréis de acuerdo en que era una característica sumamente interesante porque nos permitía configurar el comportamiento de nuestra aplicación cuando se producía un error inesperado. Jugando un poco con la configuración podíamos optar por mostrar valiosa información de depuración, como datos sobre la excepción lanzada, el punto exacto donde se produjo o la pila de ejecución, o bien páginas de error personalizadas con mensajes aptos para todos los públicos (como la ballenita voladora de Twitter u otras creativas páginas "oops!" que inundan la red).
En aplicaciones ASP.NET Core, la gestión de errores de aplicación se delega ahora a middlewares especializados que, posicionados estratégicamente en el pipeline, vigilan el resultado del proceso de las peticiones y toman el control cuando se ha producido un error. El código básico de gestión de errores viene ya incluido en las plantillas de proyectos MVC, pero creo que es interesante analizar un poco qué ha cambiado y en qué consiste la solución propuesta por este marco de trabajo.

Así que comencemos desde el principio… aunque antes, permitidme el tradicional disclaimer: ASP.NET Core todavía está en desarrollo, y algunos de los detalles que contemos a continuación aún podrían variar.

1. ¿Qué ocurre (por defecto) en ASP.NET Core cuando explota nuestra aplicación?

Para comprobarlo, creamos una aplicación ASP.NET Core MVC  vacía e introducimos la siguiente aberración en el controlador HomeController:
public IActionResult About()
{
    var j = 0;
    var i = 10/j; // Buggy code

    ViewData["Message"] = $"Your magic number is {i}.";
    return View();
}
Obviamente, al ejecutar y acceder a la ruta /home/about de nuestra aplicación, la acción lanza una excepción de división por cero y no puede continuar, pero, ¿qué recibimos desde el cliente cuando se produce este error?

Excepción no capturadaPues probablemente os pueda sorprender un poco al principio, pero en el lado cliente no recibiremos absolutamente nada. Bueno, sí, un código de error HTTP 500 acompañado de un contenido totalmente vacío, pero nada de páginas de error descriptivas o pistas que indiquen dónde puede estar el problema.

Y la explicación es realmente sencilla, pero tenemos que olvidar la estructura monolítica de System.Web, usado en ASP.NET 4 y anteriores, donde se incluían siempre todos los módulos y funcionalidades, las usáramos o no.

En ASP.NET Core, si queremos usar una funcionalidad, como puede ser la gestión de errores o cualquier otra, primero tendremos que incluirla previamente en nuestro proyecto, y después configurarla para que funcione de acuerdo a nuestras necesidades.

Pipeline sin gestión de erroresEn este caso, como no hemos configurado nada al respecto, la petición entra en el pipeline y comienza a ascender, atravesando los middlewares que tenemos configurados y acabando en el framework MVC, quien ejecuta nuestra acción. Al producirse la excepción, ésta desciende a través del pipeline recorriéndolo en sentido inverso hasta llegar al inicio del pipeline. Como ningún middleware ha tomado el control, el servidor se encuentra con la excepción y lo único que puede hacer con ella es generar el error 500 sin contenido adicional que recibimos en el lado cliente.

Simplemente, es que el servidor tiene poco más que decir ;)

2. Middlewares de depuración y gestión de errores

En ASP.NET Core podemos emular fácilmente lo que podíamos conseguir en versiones anteriores con la etiqueta <customErrors> del web.config, aunque la forma de hacerlo es bastante diferente. Como hemos adelantado anteriormente, ahora la solución a la gestión de errores la tenemos en forma de middleware.

Pipeline con gestión de erroresLa idea es la representada en el diagrama adjunto. Como se puede ver, lo que hacemos es posicionar un middleware capaz de procesar los errores justo en la entrada del pipeline, dejando pasar todas las peticiones entrantes y controlando todos los resultados salientes. Cuando un error desciende por el pipeline, este middleware será capaz de detectarlo, capturarlo y hacer algo con él, como retornar una descripción detallada del problema, o enviar al usuario una bonita página descriptiva. Básicamente, el qué hacer dependerá de si nos encontramos en un entorno de desarrollo o pruebas.

Para entornos de desarrollo, en ASP.NET Core disponemos del middleware llamado DeveloperExceptionPageMiddleware, que viene incluido de serie en el paquete "Microsoft.AspNetCore.Diagnostics". Obviamente, para usarlo tendremos que haber añadido previamente la referencia a dicho paquete en nuestro proyecto:
project.json con referencia al paquete de diagnósticos










(Nota: la captura anterior era válida antes del cambio de nombre de ASP.NET 5 a ASP.NET Core)

Por cierto, un inciso: este paquete, además del middleware de gestión de errores, incluye interesantes herramientas de depuración, por lo que vale la pena tenerlo a mano. Otro día hablaremos de algunas de ellas.

Una vez instalado el paquete, ya podemos añadir el middleware al pipeline. Para ello, siguiendo las convenciones habituales en este tipo de componentes, podemos usar el método UseDeveloperExceptionPage() sobre el IAppBuilder que recibimos en el método Configure() de la clase Startup:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // Set up other middlewares
    ...
}
Fijaos que añadimos el middleware sólo si estamos ejecutando la aplicación en entornos de desarrollo, lo cual asegurará que nadie externo a ellos podrá ver información técnica que podría comprometer la seguridad del sistema.

Si ahora ejecutamos la aplicación y accedemos a /Home/About, encontramos algo bastante más razonable y útil para nuestro trabajo :)
Página de error descriptiva
Como se puede observar, la página de error es bastante más completa que la que teníamos por defecto en versiones anteriores de ASP.NET, pues no sólo muestra la pila de ejecución sino también interesante información de contexto de la petición, como los parámetros de la consulta, las cookies y todos los encabezados enviados al servidor en la petición.

En entornos de producción utilizaremos en cambio el middleware ExceptionHandlerMiddleware, incluido en el mismo paquete "Microsoft.AspNetCore.Diagnostics". Ese módulo es capaz de capturar los errores, dejar una traza en el log del sistema e introducir en el pipeline una nueva petición, cuyo resultado será el que finalmente se envíe al cliente.

Para ver un ejemplo, añadimos primero el middleware al pipeline de la siguiente forma, de forma que sólo se aplique cuando el entorno no sea el de desarrollo:
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Home/Error");
}
imageTras incluir este código en la inicialización, si accedemos a /home/about en un entorno de producción, el middleware capturará el error y, antes de devolver nada al cliente, lanzará internamente una petición a la ruta que le indicamos, en este caso /home/error.

El resultado de la ejecución de la acción Error() en HomeController es el que será será enviado al cliente. Por ejemplo, si en esta acción simplemente hacemos un return View(), al cliente llegará el contenido de la vista /Views/Home/Error.cshtml, que podría ser similar al que vemos en la captura de pantalla adjunta (el texto "An error ocurred while processing your request").
public class HomeController: Controller
{
    ... // Other actions  

    // GET /home/error
    public IActionResult Error()
    {
        return View();
    }
}
Ojo, que cuando hablamos de "petición interna" en ningún momento quiere decir que se trate de una redirección enviada al navegador. Se trata de algo totalmente transparente para el lado cliente, es sólo una especie de petición falsa introducida en el pipeline por ExceptionHandlerMiddleware con objeto de que sea procesada por otros middlewares posteriores.

Y antes de acabar, una última observación. En los ejemplos anteriores hemos probado forzando un error en nuestra aplicación lanzando una excepción, pero, ¿y qué ocurre con otro tipo de errores, como un 404 "not found" o errores que no son propiamente dichos de la aplicación?

Pues por defecto el comportamiento es básicamente igual: se retorna el código de error (404, por ejemplo) sin ningún tipo de contenido o página que lo acompañe. De nuevo, la petición subirá por el pipeline y no será procesada por ningún middleware, lo que provocará que el servidor retorne este error.

Sin embargo, este tipo de errores no son capturables por los middlewares DeveloperExceptionPageMiddlewareExceptionHandlerMiddleware, orientados exclusivamente a la captura de excepciones de aplicación. Para gestionar errores ajenos a ella, como un error 404, hay que usar técnicas diferentes, que veremos en otra ocasión.

Publicado en Variable not found.

11 Comentarios:

Maxx dijo...

Hola Jose, un artículo magnífico como de costumbre pero tengo curiosidad sobre algo que no has comentado y creo que es interesante.

Imaginemos que quiero capturar las excepciones utilizando esta tecnica pero quiero distinguir entre tipos de excepciones distintas. Cuando se hace la petición interna a la acción HomeController.Error ¿como puedo saber exactamente la excepción que se ha producido si quiero mostrar un mensaje de error concreto para cada caso? P. Ej. imagínate que quiero mostrar vistas diferentes para cada tipo de excepción o que aparezca sólo el texto de la excepción pero sin mostrar más datos.

Muchas gracias crack

José María Aguilar dijo...

Hola!

Lo primero, muchas gracias por tus comentarios :)

Sí que se puede, a través de la colección de features del HttpContext. Dame unos días y te publico un post incluso con dedicatoria ;)

Un saludo!

Unknown dijo...

Hola Jose,

Gracias por tus aportes. Aunque es mi primer comentario en tu blog, te sigo desde hace bastante tiempo...

En este artículo nos explicas como devolver una vista al cliente en caso de error. Pero, ¿cuál sería la mejor forma de manejar esta situación si no siempre queremos devolver una vista?
Es decir, Si se trata de una llamada AJAX posiblemente lo que queramos devolver sea un JSON...
¿Serían diferentes middlewares? ¿un sólo middleware donde se compruebe si la petición es AJAX?, etc...

Saludos!

Anónimo dijo...

Si devueldes un Json a la vista puedes evaluar el resultado que te retorna en:
success: function (items, responseText) {

}

error: function (error) {
alert(error.responseText);
//ó puedes colocar
alert('Ocurrio un error');

}

José María Aguilar dijo...

Hola, José Alonso!

Muchas gracias por comentar, y por estar ahí ;)

Es una observación interesante. Efectivamente, en algunas las peticiones Ajax no tendría sentido retornar una vista sino datos estructurados (Json), mientras que en otras sí lo tendría (cuando obtienes vistas parciales para cargarlas dinámicamente en la página). No son casos fáciles de distinguir, salvo que establezcamos convenciones de algún tipo para distinguir entre uno y otro.

Pero bueno, independientemente de ello, si quisieras retornar JSON en peticiones Ajax lo podrías solucionar de varias formas.

Primero, nada impide que la acción de proceso del error (donde en el post retornamos una vista) retorne directamente datos JSON sustituyendo el return View() por return Json(algo), siempre que detectes que la petición es Ajax (por ejemplo mirando el encabezado "X-Requested-With" de la petición). Por tanto, de esta forma lo resolverías usando el middleware visto en este post.

También podrías hacerlo de forma genérica mediante un middleware personalizado en el pipeline, desde donde podrías capturar las excepciones salientes de peticiones Ajax y reemplazar la salida por un contenido JSON personalizado. Aunque obviamente esta solución es más trabajosa que la anterior, tampoco añade mucha complejidad al proyecto.

Un saludo!

José María Aguilar dijo...

Vaya, casi se han cruzado los comentarios :)

Gracias, anónimo (¡¡poned los nombres!!), por tu aportación!

Juan dijo...

Hola José esta ves va con Nombre!!
Buen artículo adicional a ello como se puede manejar la globalización ya que en la versión anterior se seteaba en el web.config
Otra consulta he actualizado la version de packete Beta5 a 1.0.0-rc1-final y he tenido un error al momento de ejecutar(la clásica pantalla amarilla)

No se puede cargar el tipo 'Microsoft.Dnx.Host.Clr.EntryPoint' del ensamblado'Microsoft.Dnx.Host.Clr, Version=1.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60'.
estoy con Windows 8 y Iss8
PD: el archivo Project.json he colocado "commands": {
"web": "Microsoft.AspNet.Server.Kestrel --config hosting.ini",
"ef": "EntityFramework.Commands"
},

José María Aguilar dijo...

Hola, Juan! :)

El problema en la carga es porque en la beta 8 cambiaron la forma de arrancar las aplicaciones, que ahora se basa en Ketrel. Para mograr, lo más sencillo es que crees un nuevo proyecto con la RC y copies sobre él tus archivos.

En cuanto a la globalización, también ha cambiado totalmente. Estoy preparando un post sobre ello ;)

Muchas gracias por comentar, un saludo!

Juan dijo...

Gracias José, he realizado lo inicado pero sigo teniendo el mismo problema No se puede cargar el tipo 'Microsoft.Dnx.Host.Clr.EntryPoint' del ensamblado'Microsoft.Dnx.Host.Clr, Version=1.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60'.. Esta este paquete es el que tengo instalado 1.0.0-rc1-final

Podrías indicarme si estos datos son correctos

"commands": {
"web": "Microsoft.AspNet.Server.Kestrel --config hosting.ini",
"ef": "EntityFramework.Commands"
},

José María Aguilar dijo...

Hola!

A mi me funciona bien en rc1-final tal y como lo tienes, aunque eliminando el parámetro "--config" del comando "web", pero no creo que tenga nada que ver con el error que te está apareciendo.

Seguro que tiene que ver con los cambios del modelo de hosting que introdujeron en beta 8, que requiere algunas modificaciones en project.json y en el código de la clase de Startup. Por eso te comentaba que lo más sencillo era crear un proyecto en "rc-final" desde cero, probar que todo te funciona bien (si no es así, hay algo mal en tu instalación), y después copiar sobre él los archivos de tu aplicación (controladores, vistas, modelo) pero dejando todo lo relativo a la configuración.

Aquí hablan un poco sobre ello: https://github.com/aspnet/dnx/issues/2790

Saludos!

Juan dijo...

Hola José, por fin!!!! tuve que instalar WebToolsExtensionsVS14 con ello todo bien.