Home > Development > Exception Handling in ASP.NET

Exception Handling in ASP.NET


While running one or more ASP.NET websites, it is valuable to have a system where errors encountered by your users are automatically reported so that you can fix them. It is bad practice to let the user’s see the error and it is horribly evil to force them to report the errors themselves. The answer: an automatic exception handler. I developed this handler for use in websites so that any error encountered would be e-mailed to a list of developers as well as recorded to the server logs if desired.

I also give the option of filtering out 404 (Page not found) and 403 (access denied) errors in case you only care about hard exceptions. In the past, I did see one application where we used 404 handling because the structure of the site changed. We implemented an automatic redirection for users who had bookmarks, and we caught 404 errors to find gaps in our redirector.

Let’s start with the meat of the project – the “handler”. The best way to implement our handler is to create a class that uses the current System.Web.HttpContext to get the last server error and then build XML based on that error and a few settings. The code then applies a customizable XSLT to the error xml and sends out the resulting html to a list of e-mail addresses. This code relies heavily on a custom configuration.

public static class ExceptionHandler
    {
        /// <summary>
        /// Handle the current server error
        /// </summary>
        public static void Handle()
        {
            if (System.Web.HttpContext.Current == null)
            {
                throw new System.ArgumentException("System.Web.HttpContext is null.  Please use the override with the exception as an input parameter.");
            }
            System.Web.HttpContext current = System.Web.HttpContext.Current;
            ExceptionHandlerConfiguration config = Configuration.ExceptionHandlerConfiguration;
            Exception exception = current.Server.GetLastError().GetBaseException();

            try
            {
                System.Web.HttpException httpError = (System.Web.HttpException)exception;

                int statusCode = httpError.GetHttpCode();

                if (statusCode == 404 && !config.Handle404)
                {
                    //if handle404 is false, do not handle a 404 error
                    return;
                }
                else if (statusCode == 403 || current.Response.StatusCode == 403)
                {
                    //do not handle 403 (access denied) errors
                    return;
                }
            }
            catch
            {
                //do nothing - if it errors it is not a http exception
            }

            //build a "unique" error string
            string currentError = exception.Message + exception.StackTrace + exception.TargetSite.ToString();

            //if no error is set in this session, set the error and then continue
            if (current.Session["LastException"] == null)
            {
                //add the error into the session
                current.Session["LastException"] = currentError;
            }
            else
            {
                //if the last error that happened has the same "unique" string, do not handle
                //this will guard agains error spam for the current session.  Starting a new
                //session will reset this value and let the error ocurr
                if (String.Compare(current.Session["LastException"].ToString(), currentError) == 0)
                {
                    return;
                }
            }

            #region Event Logging
            //only log if logging enabled
            if (config.LogErrors)
            {
                try
                {
                    string source = current.Request.PhysicalApplicationPath;

                    if (!String.IsNullOrEmpty(Configuration.ExceptionHandlerConfiguration.WebsiteName))
                    {
                        source = Configuration.ExceptionHandlerConfiguration.WebsiteName;
                    }

                    StringBuilder message = new StringBuilder();

                    message.Append("Message:\n");
                    message.Append(exception.Message);
                    message.Append("\n\nStack Trace:\n");
                    message.Append(exception.StackTrace);
                    message.Append("\n\nFile:\n");
                    message.Append(current.Request.PhysicalPath);
                    message.Append("\n\nSource:\n");
                    message.Append(exception.Source);
                    message.Append("\n\nLocation:\n");
                    message.Append(exception.TargetSite);

                   
                    //only log if threshold has not been met
                    EventLog.WriteEntry(source, message.ToString(), EventLogEntryType.Error);

                }
                catch
                {
                    //eat it
                }
            }
         #endregion

            #region Email Error Messaging
            //only email if threshold has not been met
            XmlDocument xdoc = BuildErrorXml();

            XslCompiledTransform transform = BuildXsl();

            StringBuilder sb = new StringBuilder();
            System.IO.StringWriter sw = new System.IO.StringWriter(sb, CultureInfo.InvariantCulture);

            transform.Transform(xdoc, null, sw);

            StringBuilder subject = new StringBuilder();

            subject.Append(current.Server.MachineName);
            subject.Append(" Application Error");

            if (!String.IsNullOrEmpty(Configuration.ExceptionHandlerConfiguration.WebsiteName))
            {
                subject = new StringBuilder();
                subject.Append(current.Server.MachineName);
                subject.Append(": ");
                subject.Append(Configuration.ExceptionHandlerConfiguration.WebsiteName);
                subject.Append(" Error");
            }

            try
            {
                Olympus.Email.SendHtml(Configuration.ExceptionHandlerConfiguration.DistributionList,
                    Configuration.ExceptionHandlerConfiguration.EmailFromAddress, subject.ToString(), sb.ToString());
            }
            catch
            {
                //eat it
            }
            #endregion
           
        }

        /// <summary>
        /// Build the error XML
        /// </summary>
        /// <returns>System.Xml.XmlDocument</returns>
        private static XmlDocument BuildErrorXml()
        {
            System.Web.HttpContext current = System.Web.HttpContext.Current;

            Exception ex = current.Error.GetBaseException();

            XmlDocument xdoc = new XmlDocument();

            // Create the XML declaration
            XmlDeclaration xmlDeclaration = xdoc.CreateXmlDeclaration("1.0", "utf-8", null);

            // Create the root element
            XmlElement rootNode = xdoc.CreateElement("exception");
            xdoc.InsertBefore(xmlDeclaration, xdoc.DocumentElement);
            xdoc.AppendChild(rootNode);

            AddNode("message", ex.Message, xdoc, rootNode);
            AddNode("stackTrace", ex.StackTrace, xdoc, rootNode);
            AddNode("source", ex.Source, xdoc, rootNode);
            AddNode("helpLink", ex.HelpLink, xdoc, rootNode);
            AddNode("targetSite", ex.TargetSite.ToString(), xdoc, rootNode);
            AddNode("errorType", ex.GetType().ToString(), xdoc, rootNode);

            AddNode("machineName", current.Server.MachineName, xdoc, rootNode);
            AddNode("timeStamp", current.Timestamp.ToString(), xdoc, rootNode);
            AddNode("userName", current.User.Identity.Name, xdoc, rootNode);

            AddNode("browser", current.Request.Browser.Browser, xdoc, rootNode);
            AddNode("httpMethod", current.Request.HttpMethod, xdoc, rootNode);
            AddNode("path", current.Request.Path, xdoc, rootNode);
            AddNode("pathInfo", current.Request.PathInfo, xdoc, rootNode);
            AddNode("rawUrl", current.Request.RawUrl, xdoc, rootNode);
            AddNode("requestType", current.Request.RequestType, xdoc, rootNode);
            AddNode("totalBytes", current.Request.TotalBytes.ToString(CultureInfo.InvariantCulture), xdoc, rootNode);

            string referrer = "Not Available";
            if (current.Request.UrlReferrer != null)
            {
                referrer = current.Request.UrlReferrer.AbsoluteUri;
            }

            AddNode("referrer", referrer, xdoc, rootNode);

            AddNode("url", current.Request.Url.AbsoluteUri, xdoc, rootNode);
            AddNode("userAgent", current.Request.UserAgent, xdoc, rootNode);
            AddNode("hostAddress", current.Request.UserHostAddress, xdoc, rootNode);
            AddNode("hostName", current.Request.UserHostName, xdoc, rootNode);
            AddNode("filePath", current.Request.FilePath, xdoc, rootNode);
            AddNode("isSecureConnection", current.Request.IsSecureConnection.ToString(CultureInfo.InvariantCulture), xdoc, rootNode);
            AddNode("physicalPath", current.Request.PhysicalApplicationPath, xdoc, rootNode);
            AddNode("contentLength", current.Request.ContentLength.ToString(CultureInfo.InvariantCulture), xdoc, rootNode);
            AddNode("contentType", current.Request.ContentType, xdoc, rootNode);
            AddNode("contentEncoding", current.Request.ContentEncoding.EncodingName, xdoc, rootNode);

            #region QueryString
            // Create a new <Category> element and add it to the root node
            XmlElement queryStringRootNode = xdoc.CreateElement("queryStrings");
            xdoc.DocumentElement.PrependChild(queryStringRootNode);

            foreach (string key in current.Request.QueryString.AllKeys)
            {
                // Create a new <Category> element and add it to the root node
                XmlElement qsNode = xdoc.CreateElement("queryString");

                // Set attribute name and value
                qsNode.SetAttribute("id", key);
                qsNode.SetAttribute("value", current.Request.QueryString[key]);

                //append node
                queryStringRootNode.AppendChild(qsNode);
            }
            #endregion

            #region Cookies
            // Create a new <Category> element and add it to the root node
            XmlElement cookieRootNode = xdoc.CreateElement("cookies");
            xdoc.DocumentElement.PrependChild(cookieRootNode);

            foreach (string key in current.Request.Cookies)
            {
                // Create a new <Category> element and add it to the root node
                XmlElement cookieNode = xdoc.CreateElement("cookie");

                // Set attribute name and value
                cookieNode.SetAttribute("id", key);
                cookieNode.SetAttribute("value", current.Request.Cookies[key].Value);

                //append node
                cookieRootNode.AppendChild(cookieNode);
            }
            #endregion

            #region Decrypted Cookies
            // Create a new <Category> element and add it to the root node
            XmlElement decryptedCookieRootNode = xdoc.CreateElement("decryptedCookies");
            xdoc.DocumentElement.PrependChild(decryptedCookieRootNode);

            foreach (string encryptedKey in current.Request.Cookies)
            {
                try
                {
                    string key = Olympus.Encryption.DecryptQueryString(encryptedKey);

                    // Create a new <Category> element and add it to the root node
                    XmlElement cookieNode = xdoc.CreateElement("cookie");

                    // Set attribute name and value
                    cookieNode.SetAttribute("id", key);
                    //cookieNode.SetAttribute("value", current.Request.Cookies[key].Value);

                    //append node
                    decryptedCookieRootNode.PrependChild(cookieNode);

                    if (current.Request.Cookies[encryptedKey].Values.Count > 0)
                    {
                        foreach (string encryptedAttribute in current.Request.Cookies[encryptedKey].Values.AllKeys)
                        {
                            string attribute = Olympus.Encryption.DecryptQueryString(encryptedAttribute);

                            XmlElement valueNode = xdoc.CreateElement("cookieValue");
                            valueNode.SetAttribute("id", attribute);
                            valueNode.SetAttribute("value", Olympus.Encryption.DecryptQueryString(current.Request.Cookies[encryptedKey].Values[encryptedAttribute]));
                            cookieNode.AppendChild(valueNode);
                        }
                    }
                }
                catch
                {
                    //eat the error - it is not encrypted properly so doesn't need to be added
                }
            }
            #endregion

            #region Form Data
            // Create a new <Category> element and add it to the root node
            XmlElement formRootNode = xdoc.CreateElement("formData");
            xdoc.DocumentElement.PrependChild(formRootNode);

            foreach (string key in current.Request.Form)
            {
                // Create a new <Category> element and add it to the root node
                XmlElement formNode = xdoc.CreateElement("cookie");

                // Set attribute name and value
                formNode.SetAttribute("id", key);
                formNode.SetAttribute("value", current.Request.Form[key]);

                //append node
                formRootNode.AppendChild(formNode);
            }
            #endregion

            #region Http Headers
            // Create a new <Category> element and add it to the root node
            XmlElement headerRootNode = xdoc.CreateElement("headers");
            xdoc.DocumentElement.PrependChild(headerRootNode);

            foreach (string key in current.Request.Headers)
            {
                // Create a new <Category> element and add it to the root node
                XmlElement headerNode = xdoc.CreateElement("header");

                // Set attribute name and value
                headerNode.SetAttribute("id", key);
                headerNode.SetAttribute("value", current.Request.Headers[key]);

                //append node
                headerRootNode.AppendChild(headerNode);
            }
            #endregion

            return xdoc;
        }

        /// <summary>
        /// Add a node
        /// </summary>
        /// <param name="name">the name of the node</param>
        /// <param name="data">the data to set in the node</param>
        /// <param name="xdoc">the document the node belongs to</param>
        /// <param name="parentNode">the parent node of this node</param>
        private static void AddNode(string name, string data, XmlDocument xdoc, XmlElement parentNode)
        {
            XmlElement newNode = xdoc.CreateElement(name);

            XmlText newText = xdoc.CreateTextNode(data);

            parentNode.AppendChild(newNode);

            newNode.AppendChild(newText);
        }

        /// <summary>
        /// Build the XSL Transformation
        /// </summary>
        /// <returns>System.Xml.XslCompiledTransform</returns>
        private static XslCompiledTransform BuildXsl()
        {
            if (!String.IsNullOrEmpty(Configuration.ExceptionHandlerConfiguration.Xslt))
            {
                try
                {
                    //try to use the input url
                    XslCompiledTransform transform = XslFromUrl(Configuration.ExceptionHandlerConfiguration.Xslt);

                    if (transform != null)
                    {
                        //only return if not null, else return the default
                        return transform;
                    }
                }
                catch
                {
                    //eat it = the url wasn't valid
                    //the default will be used instead
                }
            }

            return DefaultXsl();
        }

        /// <summary>
        /// Get the default XSL Transformation
        /// </summary>
        /// <returns></returns>
        private static XslCompiledTransform DefaultXsl()
        {
            XslCompiledTransform transform = new XslCompiledTransform();

            System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
            System.IO.Stream stream = assembly.GetManifestResourceStream("Olympus.Exceptions.Xslt.errorXSL.xslt");

            XmlTextReader reader = new XmlTextReader(stream);

            transform.Load(reader);

            return transform;
        }

        /// <summary>
        /// Get the XSL stylesheet from a URL
        /// </summary>
        /// <param name="url">the url of the xsl stylesheet</param>
        /// <returns>System.Xml.XslCompiledTransform</returns>
        private static XslCompiledTransform XslFromUrl(string url)
        {
            XslCompiledTransform transform = new XslCompiledTransform();
            System.Web.HttpContext current = System.Web.HttpContext.Current;

            try
            {
                string xslUrl = url.ToLower(CultureInfo.InvariantCulture);

                //the url must have the full address
                //if it doesn't, build the full address
                if (xslUrl.IndexOf("http://") < 0)
                {
                    StringBuilder host = new StringBuilder();

                    host.Append("http://");
                    host.Append(current.Request.Url.Host);

                    if (current.Request.Url.Port != 80)
                    {
                        host.Append(":");
                        host.Append(current.Request.Url.Port);
                    }

                    if (String.Compare(xslUrl.Substring(0,1), "/") != 0)
                    {
                        host.Append("/");
                    }

                    host.Append(xslUrl);

                    xslUrl = host.ToString();
                }

                XmlTextReader reader = new XmlTextReader(xslUrl);

                transform.Load(reader);
            }
            catch
            {
                //return null if an error occurs
                return null;
            }

            return transform;
        }
    }

If you follow through the code, you will see the logic is fairly simple.  For the error, I get out all the data about the error, including cookie values, querystring values, headers, and form data.  Decrypted cookie values are cookies encrypted using Rijndael. This simply tries to decrypt the cookies so that I can see their value for easy debugging.
I also have the option of logging the data to the server error logs. For my methods I don’t log as much data as I place in the e-mail, but you could modify it to place whatever data you want in the log.

Once I have built the XML with all of the exception data, I apply the XSLT. I have a default XSLT used unless the user overrides it with a relative url in the web.config.  This ensures that every site using my handler will always spit out html e-mails instead of sending you xml. The trick to this code is that the global.asax is used to call the handler in the Application_Error method. Any time the ASP.NET has an error that is not caught in the code, this method is called.


void Application_Error(object sender, EventArgs e)
{
        Olympus.Exceptions.ExceptionHandler.Handle();
}

In conjunction with the handler, it is a good practice to use the customErrors tags in the web.config to declare the error pages for a 404, 403, and 500 error. In IIS you will also want to set your custom error pages to redirect to those pages for the site, incase the error is not handled by ASP.NET, such as in the case of a misspelled url. Once the code in the exception handler has executed, if you have set the custom errors, the user will be redirected automatically.

<customErrors mode="On">
 <error statusCode="403" redirect="/Error/403.aspx"/>
 <error statusCode="404" redirect="/Error/404.aspx"/>
 <error statusCode="500" redirect="/Error/500.aspx"/>
</customErrors>

With an error handler, you can stay on top of your code, have all the information needed to efficiently and accurately debug any issues, and, when using the power of customized configuration, you can change who recieves the error notifications as projects pass hands.

Advertisements
Categories: Development Tags: , , , ,
  1. No comments yet.
  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: