Procesar excepciones no controladas (C#)

por Scott Mitchell

Vea o descargue el código de ejemplo (cómo descargarlo)

Cuando se produce un error en runtime en una aplicación web en producción, es importante notificar a un desarrollador y registrar el error para que se pueda diagnosticar en un momento posterior. En este tutorial se proporciona información general sobre cómo ASP.NET procesa los errores en runtime y examina una manera de ejecutar código personalizado cada vez que se propaga una excepción no controlada hasta el runtime de ASP.NET.

Introducción

Cuando se produce una excepción no controlada en una aplicación de ASP.NET, se propaga hasta el runtime de ASP.NET, lo que genera el evento Error y muestra la página de error adecuada. Hay tres tipos diferentes de páginas de error: la Error Yellow Screen of Death (YSOD) en runtime; la YSOD de detalles de excepción; y páginas de error personalizadas. En el tutorial anterior hemos configurado la aplicación para usar una página de error personalizada para usuarios remotos y la YSOD de detalles de excepción para los usuarios que visitan localmente.

Es preferible usar una página de error personalizada que se adapte a la apariencia del sitio en lugar de la YSOD de error en runtime predeterminada, pero mostrar una página de error personalizada es solo una parte de una solución integral de control de errores. Cuando se produce un error en una aplicación en producción, es importante que los desarrolladores notifiquen el error para que puedan desencerrar la causa de la excepción y solucionarlo. Además, los detalles del error deben registrarse para poder examinarlo y diagnosticarlo más adelante.

En este tutorial se muestra cómo acceder a los detalles de una excepción no controlada para que se puedan registrar y notificar a un desarrollador. En los dos tutoriales siguientes a este se exploran las bibliotecas de registro de errores que, después de una configuración, notificarán automáticamente a los desarrolladores de errores en runtime y registrarán sus detalles.

Nota:

La información que se examina en este tutorial es más útil si necesita procesar excepciones no controladas de alguna manera única o personalizada. En los casos en los que solo necesite registrar la excepción y notificar a un desarrollador, el uso de una biblioteca de registro de errores es el camino a seguir. En los dos tutoriales siguientes se proporciona información general sobre dos bibliotecas de este tipo.

Ejecución de código cuando se genera el evento Error

Los eventos proporcionan a un objeto un mecanismo para indicar que se ha producido algo interesante y para que otro objeto ejecute código en respuesta. Como desarrollador ASP.NET está acostumbrado a pensar en términos de eventos. Si desea ejecutar código cuando el visitante hace clic en un botón determinado, cree un controlador de eventos para ese evento Click de ese botón y coloque el código allí. Dado que el runtime de ASP.NET lanza su Errorevento cada vez que se produce una excepción no controlada, se deduce que el código para registrar los detalles del error iría en un controlador de eventos. ¿Pero cómo se crea un controlador de eventos para el evento Error?

El evento Error es uno de los muchos eventos de la HttpApplicationclase que se generan en determinadas fases de la canalización HTTP durante la vigencia de una solicitud. Por ejemplo, el BeginRequestevento de la clase HttpApplication se genera al principio de cada solicitud; su AuthenticateRequestevento se genera cuando un módulo de seguridad ha identificado al solicitante. Estos eventos HttpApplication proporcionan al desarrollador de páginas un medio para ejecutar lógica personalizada en los distintos puntos de la duración de una solicitud.

Los controladores de eventos de los eventos HttpApplication se pueden colocar en un archivo especial denominado Global.asax. Para crear este archivo en el sitio web, agregue un nuevo elemento a la raíz del sitio web mediante la plantilla Clase de aplicación global con el nombre Global.asax.

Sceenshot that highlights the Global dot A S A X file.

Ilustración 1: agregar Global.asax a la aplicación web
(Haga clic para ver la imagen a tamaño completo.)

El contenido y la estructura del archivo Global.asax creado por Visual Studio difieren ligeramente en función de si se usa un proyecto de aplicación web (WAP) o un proyecto de sitio web (WSP). Con un WAP, Global.asax se implementa como dos archivos independientes: Global.asax y Global.asax.cs. El archivo Global.asax no contiene nada más que una directiva @Application que hace referencia al archivo .cs; los controladores de eventos de interés se definen en el archivo Global.asax.cs. En el caso de los WSP, solo se crea un único archivo, Global.asax, y los controladores de eventos se definen en un bloque <script runat="server">.

El archivo Global.asax creado en una plantilla de clase de aplicación global de WAP de Visual Studio incluye controladores de eventos denominados Application_BeginRequest, Application_AuthenticateRequest y Application_Error, que son controladores de eventos para los eventos HttpApplication, BeginRequest, AuthenticateRequesty Error, respectivamente. También hay controladores de eventos denominados Application_Start, Session_Start, Application_End y Session_End, que son controladores de eventos que se activan cuando se inicia la aplicación web, cuando se inicia una nueva sesión, cuando finaliza la aplicación y cuando finaliza una sesión, respectivamente. El archivo Global.asax creado en un WSP de Visual Studio contiene solo los controladores de eventos Application_Error, Application_Start, Session_Start, Application_End y Session_End.

Nota:

Al implementar la aplicación ASP.NET, debe copiar el archivo Global.asax en el entorno de producción. El archivo Global.asax.cs, que se crea en el WAP, no es necesario copiarlo en producción porque este código se compila en el ensamblado del proyecto.

Los controladores de eventos creados por la plantilla de clase de aplicación global de Visual Studio no son exhaustivos. Para agregar un controlador de eventos para cualquier evento HttpApplication, asigne un nombre al controlador de eventos Application_EventName. Por ejemplo, puede agregar el siguiente código al archivo Global.asax para crear un controlador de eventos para el evento AuthorizeRequest:

protected void Application_AuthorizeRequest(object sender, EventArgs e)
{
    // Event handler code
}

Del mismo modo, puede quitar cualquier controlador de eventos creado por la plantilla clase de aplicación global que no sean necesarios. En este tutorial solo se requiere un controlador de eventos para el evento Error; no dude en quitar los otros controladores de eventos del archivo Global.asax.

Nota:

Los módulos HTTP ofrecen otra manera de definir controladores de eventos para eventos HttpApplication. Los módulos HTTP se crean como un archivo de clase que se puede colocar directamente dentro del proyecto de aplicación web o se separan en una biblioteca de clases independiente. Dado que se pueden separar en una biblioteca de clases, los módulos HTTP ofrecen un modelo más flexible y reutilizable para crear controladores de eventos HttpApplication. Mientras que el archivo Global.asax es específico de la aplicación web donde reside, los módulos HTTP se pueden compilar en ensamblados, en cuyo punto agregar el módulo HTTP a un sitio web es tan sencillo como quitar el ensamblado en la carpeta Bin y registrar el módulo en Web.config. En este tutorial no se examina la creación y el uso de módulos HTTP, pero las dos bibliotecas de registro de errores usadas en los dos tutoriales siguientes se implementan como módulos HTTP. Para obtener más información sobre las ventajas de los módulos HTTP, consulte Uso de módulos y controladores HTTP para crear componentes de ASP.NET conectables.

Recuperación de información sobre la excepción no controlada

En este momento tenemos un archivo Global.asax con un controlador de eventos Application_Error. Cuando este controlador de eventos se ejecuta, es necesario notificar al desarrollador el error y registrar sus detalles. Para realizar estas tareas, primero es necesario determinar los detalles de la excepción que se generó. Use el método GetLastError del objeto Server para recuperar los detalles de la excepción no controlada que provocó que se desencadenara el evento Error.

protected void Application_Error(object sender, EventArgs e)
{
    // Get the error details
    HttpException lastErrorWrapper = 
        Server.GetLastError() as HttpException;
}

El método GetLastError devuelve un objeto de tipo Exception, que es el tipo base para todas las excepciones de .NET Framework. Sin embargo, en el código anterior estoy convirtiendo el objeto Exception devuelto por GetLastError en un objeto HttpException. Si se desencadena el evento Error porque se produjo una excepción durante el procesamiento de un recurso de ASP.NET, la excepción que se produjo se encapsula dentro de un HttpException. Para obtener la excepción real que precipitaba el evento Error, use la propiedad InnerException. Si el evento Error se generó debido a una excepción basada en HTTP, como una solicitud de una página inexistente, se produce un HttpException, pero no tiene una excepción interna.

El siguiente código usa el GetLastErrormessage para recuperar información sobre la excepción que desencadenó el evento Error, almacenando el HttpException en una variable denominada lastErrorWrapper. A continuación, almacena el tipo, el mensaje y el seguimiento de pila de la excepción de origen en tres variables de cadena, comprobando si el lastErrorWrapper es la excepción real que desencadenó el evento Error (en el caso de excepciones basadas en HTTP) o si es simplemente un contenedor para una excepción que se produjo mientras se procesaba la solicitud.

protected void Application_Error(object sender, EventArgs e)
{
    // Get the error details
    HttpException lastErrorWrapper = 
        Server.GetLastError() as HttpException;

    Exception lastError = lastErrorWrapper;
    if (lastErrorWrapper.InnerException != null)
        lastError = lastErrorWrapper.InnerException;

    string lastErrorTypeName = lastError.GetType().ToString();
    string lastErrorMessage = lastError.Message;
    string lastErrorStackTrace = lastError.StackTrace;
}

En este momento, tiene toda la información que necesita para escribir código que registrará los detalles de la excepción en una tabla de base de datos. Puede crear una tabla de base de datos con columnas para cada uno de los detalles de error de interés (el tipo, el mensaje, el seguimiento de la pila, etc.) junto con otros elementos útiles de información, como la dirección URL de la página solicitada y el nombre del usuario que ha iniciado sesión actualmente. En el controlador de eventos Application_Error, se conectaría a la base de datos e insertaría un registro en la tabla. Del mismo modo, podría agregar código para alertar a un desarrollador del error por correo electrónico.

Las bibliotecas de registro de errores que se examinan en los dos tutoriales siguientes proporcionan esta funcionalidad de forma predeterminada, por lo que no es necesario crear este registro de errores y notificaciones. Sin embargo, para ilustrar que se está generando el evento Error y que el controlador de eventos Application_Error se puede usar para registrar los detalles del error y notificar a un desarrollador, vamos a agregar código que notifica a un desarrollador cuando se produce un error.

Envío de una notificación a un desarrollador cuando se produce una excepción no controlada

Cuando se produce una excepción no controlada en el entorno de producción, es importante alertar al equipo de desarrollo para que pueda evaluar el error y determinar qué acciones deben realizarse. Por ejemplo, si se produce un error al conectarse a la base de datos, deberá comprobar la cadena de conexión y, quizás, abrir una incidencia de soporte técnico con la empresa de hospedaje web. Si se produjo la excepción debido a un error de programación, es posible que sea necesario agregar código adicional o lógica de validación para evitar dichos errores en el futuro.

Las clases de .NET Framework del espacio de nombres System.Net.Mail facilitan el envío de un correo electrónico. La clase MailMessage representa un mensaje de correo electrónico y tiene propiedades como To, From, Subject, Body y Attachments. El SmtpClass se usa para enviar un objeto MailMessage mediante un servidor SMTP especificado; la configuración del servidor SMTP se puede especificar mediante programación o mediante declaración en el <system.net> elemento del Web.config file. Para obtener más información sobre cómo enviar mensajes de correo electrónico en una aplicación de ASP.NET consulte mi artículo, Cómo enviar correo electrónico desde un sitio de páginas web ASP.NET y las preguntas más frecuentes de System.Net.Mail.

Nota:

El elemento <system.net> contiene la configuración del servidor SMTP utilizada por la clase SmtpClient al enviar un correo electrónico. Es probable que la empresa de hospedaje web tenga un servidor SMTP que pueda usar para enviar correo electrónico desde la aplicación. Consulte la sección de soporte de su proveedor de hospedaje web para obtener información sobre la configuración del servidor SMTP que debe utilizar en su aplicación web.

Agregue el código siguiente al controlador de eventos Application_Error para enviar al desarrollador un correo electrónico cuando se produzca un error:

void Application_Error(object sender, EventArgs e)
{
    // Get the error details
    HttpException lastErrorWrapper = 
        Server.GetLastError() as HttpException;

    Exception lastError = lastErrorWrapper;
    if (lastErrorWrapper.InnerException != null)
        lastError = lastErrorWrapper.InnerException;

    string lastErrorTypeName = lastError.GetType().ToString();
    string lastErrorMessage = lastError.Message;
    string lastErrorStackTrace = lastError.StackTrace;

    const string ToAddress = "support@example.com";
    const string FromAddress = "support@example.com";
    const string Subject = "An Error Has Occurred!";
    
    // Create the MailMessage object
    MailMessage mm = new MailMessage(FromAddress, ToAddress);
    mm.Subject = Subject;
    mm.IsBodyHtml = true;
    mm.Priority = MailPriority.High;
    mm.Body = string.Format(@"
<html>
<body>
  <h1>An Error Has Occurred!</h1>
  <table cellpadding=""5"" cellspacing=""0"" border=""1"">
  <tr>
  <tdtext-align: right;font-weight: bold"">URL:</td>
  <td>{0}</td>
  </tr>
  <tr>
  <tdtext-align: right;font-weight: bold"">User:</td>
  <td>{1}</td>
  </tr>
  <tr>
  <tdtext-align: right;font-weight: bold"">Exception Type:</td>
  <td>{2}</td>
  </tr>
  <tr>
  <tdtext-align: right;font-weight: bold"">Message:</td>
  <td>{3}</td>
  </tr>
  <tr>
  <tdtext-align: right;font-weight: bold"">Stack Trace:</td>
  <td>{4}</td>
  </tr> 
  </table>
</body>
</html>",
        Request.RawUrl,
        User.Identity.Name,
        lastErrorTypeName,
        lastErrorMessage,
        lastErrorStackTrace.Replace(Environment.NewLine, "<br />"));

    // Attach the Yellow Screen of Death for this error   
    string YSODmarkup = lastErrorWrapper.GetHtmlErrorMessage();
    if (!string.IsNullOrEmpty(YSODmarkup))
    {
        Attachment YSOD = 
            Attachment.CreateAttachmentFromString(YSODmarkup, "YSOD.htm");
        mm.Attachments.Add(YSOD);
    }

    // Send the email
    SmtpClient smtp = new SmtpClient();
    smtp.Send(mm);
}

Aunque el código anterior es bastante largo, la mayor parte crea el código HTML que aparece en el correo electrónico enviado al desarrollador. El código comienza haciendo referencia al HttpException devuelto por el método GetLastError (lastErrorWrapper). La excepción real que generó la solicitud se recupera a través de lastErrorWrapper.InnerException y se asigna a la variable lastError. La información de seguimiento de tipo, mensaje y pila se recupera de lastError y se almacena en tres variables de cadena.

A continuación, se crea un objeto MailMessage denominado mm. El cuerpo del correo electrónico tiene formato HTML y muestra la dirección URL de la página solicitada, el nombre del usuario que ha iniciado sesión actualmente e información sobre la excepción (el tipo, el mensaje y el seguimiento de pila). Una de las cosas interesantes de la clase HttpException es que puede generar el HTML usado para crear la Yellow Screen of Death (YSOD) de detalles de excepción llamando al método GetHtmlErrorMessage. Este método se usa aquí para recuperar el marcado de la YSOD de detalles de excepción y agregarlo al correo electrónico como datos adjuntos. Una advertencia: si la excepción que desencadenó el evento Error era una excepción basada en HTTP (por ejemplo, una solicitud para una página inexistente), el método GetHtmlErrorMessage devolverá null.

El último paso es enviar el MailMessage. Para ello, se crea un nuevo método SmtpClient y se llama a su método Send.

Nota:

Antes de usar este código en la aplicación web, querrá cambiar los valores de la ToAddress y FromAddress constantes de support@example.com a cualquier dirección de correo electrónico a la que se debe enviar y originar el correo electrónico de notificación de error. También deberá especificar la configuración del servidor SMTP en la sección <system.net> de Web.config. Consulte al proveedor de host web para determinar la configuración del servidor SMTP que se va a usar.

Con este código en su lugar siempre que se produce un error, el desarrollador se envía un mensaje de correo electrónico que resume el error e incluye la YSOD. En el tutorial anterior se mostró un error en runtime visitando Genre.aspx y pasando un valor ID no válido a través de la cadena de consulta, como Genre.aspx?ID=foo. Al visitar la página con el Global.asax archivo en contexto, se produce la misma experiencia de usuario que en el tutorial anterior: en el entorno de desarrollo, seguirá viendo la YSOD de detalles de excepción mientras que en el entorno de producción verá la página de error personalizada. Además de este comportamiento existente, se le envía un correo electrónico al desarrollador.

En la ilustración 2 se muestra el correo electrónico recibido al visitar Genre.aspx?ID=foo. El cuerpo del correo electrónico resume la información de la excepción, mientras que los datos adjuntos YSOD.htm muestran el contenido que aparece en la YSOD de detalles de la excepción (véase la ilustración 3).

Screenshot that shows the email sent to the developer.

Ilustración 2: el desarrollador recibe una notificación por correo electrónico cada vez que se produce una excepción no controlada
(Haga clic para ver la imagen a tamaño completo.)

Screenshot that shows that the email notification includes the exception details Y S O D as an attachment.

Ilustración 3: la notificación por correo electrónico incluye la YSOD de detalles de excepción como datos adjuntos
(Haga clic para ver la imagen a tamaño completo.)

¿Qué ocurre con el uso de la página de error personalizado?

En este tutorial se muestra cómo usar Global.asax y el controlador de eventos Application_Error para ejecutar código cuando se produce una excepción no controlada. En concreto, usamos este controlador de eventos para notificar al desarrollador un error; podríamos ampliarlo para registrar también los detalles del error en una base de datos. La presencia del controlador de eventos Application_Error no afecta a la experiencia del usuario final. Siguen viendo la página de error configurada, ya sea la YSOD de detalles del error, la YSOD en runtime o la página de error personalizada.

Es normal preguntarse si el archivo Global.asax y el evento Application_Error son necesarios cuando se usa una página de error personalizada. Cuando se produce un error, se muestra al usuario la página de error personalizada, así que ¿por qué no podemos poner el código para notificar al desarrollador y registrar los detalles del error en la clase de código subyacente de la página de error personalizada? Aunque puede agregar código a la clase de código subyacente de la página de error personalizada, no tiene acceso a los detalles de la excepción que desencadenó el evento Error al usar la técnica que hemos explorado en el tutorial anterior. Al llamar al método GetLastError desde la página de error personalizada, se devuelve Nothing.

El motivo de este comportamiento es que se alcanza la página de error personalizada a través de un redireccionamiento. Cuando una excepción no controlada alcanza el runtime de ASP.NET, el motor de ASP.NET genera su evento Error (que ejecuta el controlador de eventos Application_Error) y, a continuación, redirige el usuario a la página de error personalizada mediante la emisión de un Response.Redirect(customErrorPageUrl). El método Response.Redirect envía una respuesta al cliente con un código de estado HTTP 302, lo que indica al explorador que solicite una nueva dirección URL, es decir, la página de error personalizada. A continuación, el explorador solicita automáticamente esta nueva página. Puede saber que la página de error personalizada se solicitó de forma independiente a la página en la que se originó el error porque la barra de direcciones del navegador cambia a la URL de la página de error personalizada (véase la ilustración 4).

Screenshot that shows that the browser gets redirected when an error occurs.

Ilustración 4: cuando se produce un error, el explorador se redirige a la dirección URL de la página de error personalizada
(Haga clic para ver la imagen a tamaño completo.)

El efecto neto es que la solicitud en la que se produjo la excepción no controlada finaliza cuando el servidor responde con la redirección HTTP 302. La solicitud posterior a la página de error personalizada es una solicitud nueva; en este punto, el motor ASP.NET ha descartado la información de error y, además, no tiene forma de asociar la excepción no gestionada en la solicitud anterior con la nueva solicitud de la página de error personalizada. Este es el motivo por el que GetLastError devuelve null cuando se llama desde la página de error personalizada.

Sin embargo, es posible ejecutar la página de error personalizada durante la misma solicitud que provocó el error. El método Server.Transfer(url) transfiere la ejecución a la dirección URL especificada y la procesa dentro de la misma solicitud. Puede mover el código del controlador de eventos de Application_Error a la clase de código subyacente de la página de error personalizada y reemplazarlo en Global.asax por el código siguiente:

protected void Application_Error(object sender, EventArgs e)
{
    // Transfer the user to the appropriate custom error page
    HttpException lastErrorWrapper = 
        Server.GetLastError() as HttpException;

    if (lastErrorWrapper.GetHttpCode() == 404)
    {
        Server.Transfer("~/ErrorPages/404.aspx");
    }
    else
    {
        Server.Transfer("~/ErrorPages/Oops.aspx");
    }
}

Ahora, cuando se produce una excepción no controlada, el controlador de eventos Application_Error transfiere el control a la página de error personalizada adecuada en función del código de estado HTTP. Dado que se ha transferido el control, la página de error personalizada tiene acceso a la información de excepción no controlada a través de Server.GetLastError y puede notificar a un desarrollador el error y registrar sus detalles. La llamada Server.Transfer impide que el motor de ASP.NET redirija al usuario a la página de error personalizada. En su lugar, el contenido de la página de error personalizada se devuelve como la respuesta a la página que generó el error.

Resumen

Cuando se produce una excepción no controlada en una aplicación web ASP.NET, el tiempo de ejecución de ASP.NET genera el evento Error y muestra la página de error configurada. Podemos notificar al desarrollador el error, registrar sus detalles o procesarlo de alguna otra manera mediante la creación de un controlador de eventos para el evento Error. Hay dos maneras de crear un controlador de eventos para eventos HttpApplication como Error: en el archivo Global.asax o desde un módulo HTTP. En este tutorial se muestra cómo crear un controlador de eventos Error en el archivo Global.asax que notifica a los desarrolladores un error mediante un mensaje de correo electrónico.

Crear un controlador de eventos Error es útil si necesita procesar excepciones no controladas de alguna manera única o personalizada. Sin embargo, crear su propio controlador de eventos Error para registrar la excepción o notificar a un desarrollador no es el uso más eficaz de su tiempo, ya que ya existe libre y fácil de usar bibliotecas de registro de errores que se pueden configurar en cuestión de minutos. En los dos tutoriales siguientes se examinan dos bibliotecas de este tipo.

¡Feliz programación!

Lecturas adicionales

Para obtener más información sobre los temas tratados en este tutorial, consulte los siguientes recursos: