I was browsing a site earlier and I came upon the dreaded Yellow Screen of Death (YSOD). It reminded me of when I had to implement some diagnostic tracing for an application that happened to be built prior to my time at the said company.
The Problem
The application was written in ASP.NET 1.1 and was recently upgraded to 2.0. There was a TON of code in the code behind, heck, we could go as far as to say that the majority of the business logic was in the code behind. The validation and error checking routines were nearly non-existant and as of lately the system was encountering a lot of YSOD screens. The users were confused and on top of that the YSOD screen was presenting TOO MUCH info, enabling a malicious user to take that info and use it for evil purposes. I HAD to get rid of these errors, and quick, and I wasnt allowed to turn off the application so I could fix it. I was allowed to take down the app for a few seconds to implement a change and thats it. I had the error report but could not
I had to implement some way of tracking these bugs without taking down the application for good. I did this through the Application_Error event in the Global.asax file.
Background Info on Page Processing
When a ASP.NET Page is processing and it encounters an error, and if it is not handled, it will present the YSOD with all the pertinent information to the user (that is, if no error handling or default error page is defined in the web.Config). In this project, that was the case. No error handling was present.
An ASP.NET page can also catch its own errors if you implement the Page_Error method. Inside of this method you can get the last error that was thrown and do something with it and then move on. Like this:
public void Page_Error(object sender, EventArgs e)
{
Exception ex = Server.GetLastError();
/// Do some stuff with the exception, such as logging, etc
// Clean up
Server.ClearError();
}
The problem with this method is that you must implement this on every page. You could do this if had a base page class you inherited from, but what if you forgot to inherit from your base page? Your page would still blow up when something went wrong and was not handled.
Enter Global.asax and Application_Error …
Implementing Application Level Error Handling & Logging
You can implement application level error handling by implementing some handling at the Application level.
Here’s some code, explanation is at the bottom.
Global.asax File
<%@ Application Language=“C#” %>
<%@ Import Namespace=“System.Web.Configuration” %>
<script runat=“server”>
void Application_Start(object sender, EventArgs e)
{
}
void Application_End(object sender, EventArgs e)
{
}
void Application_Error(object sender, EventArgs e)
{
// Perform some handling here if necessary, and then redirect to error handling page as defined in the web.Config file.
CustomErrorsSection section = (CustomErrorsSection)ConfigurationManager.GetSection(“system.web/customErrors”);
Server.Transfer(section.DefaultRedirect);
}
void Session_Start(object sender, EventArgs e)
{
}
void Session_End(object sender, EventArgs e)
{
}
</script>
web.Config File
<?xml version=“1.0”?>
<configuration>
<appSettings/>
<connectionStrings/>
<system.web>
<compilation debug=“true”/>
<authentication mode=“Windows”/>
<customErrors defaultRedirect=“Oops.aspx” />
<trace writeToDiagnosticsTrace=“true” enabled=“true”/>
</system.web>
<system.diagnostics>
<trace autoflush=“true”>
<listeners>
<add name=“TraceSiteEventLogListener” type=“System.Diagnostics.EventLogTraceListener,System,version=2.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089” initializeData=“Test123”/>
</listeners>
</trace>
</system.diagnostics>
</configuration>
Oops.aspx Page
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Web.Configuration;
public partial class Oops : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Exception ex = Server.GetLastError();
string errorInfo = GetFormattedException(ex);
Trace.Write(errorInfo);
Server.ClearError();
}
/// <summary>
/// Gets the exception information, recursively.
/// </summary>
/// <param name=”ex”>The exception that occurred.</param>
/// <returns>A formatted string that contains the message, exception type, source and stack trace for each exception
/// that exists within the exception hierarchy.</returns>
private string GetFormattedException(Exception ex)
{
string exceptionString = string.Empty;
// If we have an inner exception, we need to get that information first.
if (ex.InnerException != null)
{
exceptionString = String.Format(“Inner Exception: {0}{1}”, GetFormattedException(ex.InnerException), Environment.NewLine);
}
string exceptionFormat = “[{0}] – Message: {1} – Exception Type: {2} – Source: {3} – Stack Trace – {4}”;
exceptionString += String.Format(exceptionFormat,
DateTime.Now,
ex.Message,
ex.GetType().FullName,
ex.Source,
ex.StackTrace);
return exceptionString;
}
}
Oops.aspx
<%@ Page Language=“C#” AutoEventWireup=“true” CodeFile=“Oops.aspx.cs” Inherits=“Oops” %>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<html xmlns=“http://www.w3.org/1999/xhtml” >
<head runat=“server”>
<title>Error Page</title>
</head>
<body>
<form id=“form1” runat=“server”>
<div>
Oops! An error occurred!
</div>
</form>
</body>
</html>
Explanation
If an error is thrown, on any page, the Global.asax’s Application_Error event catches the error. At that time I redirect the user to the default error page that is defined in the web.Config’s customErrors section (Oops.aspx).
In that file (Oops.aspx) I grab the last exception by making a call to Server.GetLastError(). Then I recursively loop through each child exception that the exception has and format the data for viewing purposes.
After the data is formatted I then write it to the Page.Trace, which is set up to write it to the Trace Listeners that are set up in the web.config. I have decided to use the Event Log as my location of writing my trace information to, but this could easily be set up to use the TextWriterTraceListener (or any other listener) as well.
Then at the end, I clean up the exception queue by calling Server.ClearError().
Then, the page is displayed. “Oops! An error occurred!”.
Now, open up the event viewer and look inside of the application log. You’ll see a source by the name of “Test123” and you’ll see the error information inside. You’ll also find a plethora of other great event information too.
For this to all work, we have to set the Trace enabled to true in the web.Config and set the writeToDiagnosticsTrace equal to true as well. Otherwise the Page’s Trace will not redirect its trace information to the listeners defined in the web.Config. To turn this off, just set the enabled flag to “false”.
Conclusion
The best thing to do is to always capture the error, give the user a meaningfull message such as “First name cannot be blank.” But at times there are errors that we don’t catch as developers, things slip through the cracks and we miss something, that’s life, it happens. In event of such case, we can enable global exception handling, just like we did here. Just in case something “exceptional” happens, we can catch it, log it in the event viewer and not have to worry about displaying vital information to a user through the YSOD.
Leave a Reply
You must be logged in to post a comment.