Click here to Skip to main content
12,241,599 members (56,725 online)
Click here to Skip to main content

Stats

38.7K views
968 downloads
76 bookmarked
Posted

Automatic Error Handling

, 17 Mar 2008 CPOL
Handle web and WinForms exceptions automatically.
using System;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Configuration;
using System.Collections.Specialized;
using System.Diagnostics;

/// <summary>
/// Generic Unhandled error handling class for ASP.NET websites and web services. 
/// Intended as a last resort for errors which crash our application, so we can get feedback on what 
/// caused the error. Can log errors to a web page, to the event log, a local text file, or via email.
/// </summary>
/// <remarks>
/// to use:
/// 
/// 1) in ASP.NET applications:
/// 
///     Reference the HttpModule UehHttpModule in web.config:
///
///         &lt;httpModules&gt;
///	 	        &lt;add name="ASPUnhandledException" 
///                  type="ASPUnhandledException.UehHttpModule, ASPUnhandledException" /&gt;
///		    &lt;/httpModules&gt;
///
/// 2) in .NET Web Services:
///
///     Reference the SoapExtension UehSoapExtension in web.config:
///
///		    &lt;soapExtensionTypes&gt;
///				&lt;add type="ASPUnhandledException.UehSoapExtension, ASPUnhandledException"
///				     priority="1" group="0" /&gt;
///			&lt;/soapExtensionTypes&gt;
///
/// more background information on Exceptions at:
///   http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/exceptdotnet.asp
///
///  Jeff Atwood
///  http://www.codinghorror.com/
/// </remarks>
public class WebExceptionHandler : ErrorHandler.HandlerBase
{
	private bool _blnLogToEventLog = Config.GetBoolean( "LogToEventLog", false );
	private bool _blnLogToFile = Config.GetBoolean( "LogToFile", true );
	private bool _blnLogToEmail = Config.GetBoolean( "LogToEmail", true );
	private string _strLogFilePath = Config.GetPath( "PathLogFile" );
	private string _strIgnoreRegExp = Config.GetString( "IgnoreRegex", "" );
	private bool _blnIgnoreDebugErrors = Config.GetBoolean( "IgnoreDebug", true );

	private string _strViewstate = "";

	private const string _strViewstateKey = "__VIEWSTATE";


	/// <summary>
	/// ASP.Net web-service specific exception handler, to be called from UehSoapExtension
	/// </summary>
	/// <returns>
	/// string with "&lt;detail&gt;&lt;/detail&gt;" XML element, suitable for insertion into SOAP message
	/// </returns>
	/// <remarks>
	/// existing SOAP detail message, prior to insertion, looks like:
	/// 
	///   &lt;soap:Fault&gt;
	///     &lt;faultcode&gt;soap:Server&lt;/faultcode&gt;
	///     &lt;faultstring&gt;Server was unable to process request.&lt;/faultstring&gt;
	///     &lt;detail /&gt;    &lt;==  
	///   &lt;/soap:Fault&gt;
	/// </remarks>
	public static string HandleWebServiceException( System.Web.Services.Protocols.SoapMessage sm )
	{
		WebExceptionHandler handler = new WebExceptionHandler();

		return handler.HandleWebServiceExceptionInternal( sm );
	}

	public string HandleWebServiceExceptionInternal( System.Web.Services.Protocols.SoapMessage sm )
	{
		HandleException( sm.Exception, false );

		XmlDocument doc = new XmlDocument();
		XmlNode DetailNode = doc.CreateNode( XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace );

		XmlNode TypeNode = doc.CreateNode( XmlNodeType.Element, "ExceptionType", SoapException.DetailElementName.Namespace );
		TypeNode.InnerText = _exceptionType;
		DetailNode.AppendChild( TypeNode );

		XmlNode MessageNode = doc.CreateNode( XmlNodeType.Element, "ExceptionMessage", SoapException.DetailElementName.Namespace );
		MessageNode.InnerText = sm.Exception.Message;
		DetailNode.AppendChild( MessageNode );

		XmlNode InfoNode = doc.CreateNode( XmlNodeType.Element, "ExceptionInfo", SoapException.DetailElementName.Namespace );
		InfoNode.InnerText = _exceptionText;
		DetailNode.AppendChild( InfoNode );

		return DetailNode.OuterXml.ToString();
	}

	public static void HandleException( Exception ex )
	{
		HandleException( ex, true );
	}

	public static void HandleException( Exception ex, bool showUser )
	{
		HandleException( ex, showUser, string.Empty );
	}

	public static void HandleException( Exception ex, bool showUser, string info )
	{
		WebExceptionHandler handler = new WebExceptionHandler();
		handler.HandleWebException( ex, showUser, info );
	}

	/// <summary>
	/// ASP.Net exception handler, to be called from UehHttpModule
	/// </summary>
	public void HandleWebException( Exception ex, bool showUser, string info )
	{
		// don't bother us with debug exceptions (eg those running on localhost)
		if( _blnIgnoreDebugErrors )
		{
			if( System.Diagnostics.Debugger.IsAttached ) return;
			string strHost = HttpContext.Current.Request.Url.Host.ToLower();
			if( strHost.Equals( "localhost" ) || strHost.Equals( "127.0.0.1" ) ) return;
		}

		// turn the exception into an informative string
		try
		{
			if( string.IsNullOrEmpty( info ) )
				_exceptionText = ExceptionToString( ex );
			else
				_exceptionText = info + Environment.NewLine + Environment.NewLine + Environment.NewLine + ExceptionToString( ex );

			_exceptionType = ex.GetType().FullName;

			// ignore root exceptions
			if( _exceptionType == _rootException || _exceptionType == _rootWsException )
			{
				if( ex.InnerException != null )
				{
					_exceptionType = ex.InnerException.GetType().FullName;
				}
			}
		}
		catch( Exception e )
		{
			_exceptionText = "Error '" + e.Message + "' while generating exception string";
		}

		// some exceptions should be ignored: ones that match this regex
		// note that we are using the entire full-text string of the exception to test regex against
		// so any part of text can match.
		if( !string.IsNullOrEmpty( _strIgnoreRegExp ) )
		{
			if( Regex.IsMatch( _exceptionText, _strIgnoreRegExp, RegexOptions.IgnoreCase ) ) return;
		}

		// log this error to various locations
		System.Collections.Generic.Dictionary<string,System.IO.Stream> attachments = new System.Collections.Generic.Dictionary<string,System.IO.Stream>();

		if( !string.IsNullOrEmpty( _strViewstate ) )
			attachments.Add( "Viewstate.txt", new System.IO.MemoryStream( Encoding.UTF8.GetBytes( _strViewstate ) ) );

		Write( WebCurrentUrl(), "Unhandled ASP Exception notification - ", new string[0], attachments  );

		// display message to the user
		if( showUser ) ExceptionToPage();
	}

	/// <summary>
	/// turns exception into a formatted string suitable for display to a (technical) user
	/// </summary>
	private string FormatExceptionForUser()
	{
		StringBuilder sb = new StringBuilder();
		const string strBullet = "�";

		sb.Append( Environment.NewLine );
		sb.Append( "The following information about the error was automatically captured: " );
		sb.Append( Environment.NewLine );
		sb.Append( Environment.NewLine );
		if( _blnLogToEventLog )
		{
			sb.Append( " " );
			sb.Append( strBullet );
			sb.Append( " " );
			if( string.IsNullOrEmpty( _results[ "LogToEventLog" ] ) )
			{
				sb.Append( "an event was written to the application log" );
			}
			else
			{
				sb.Append( "an event could NOT be written to the application log due to an error:" );
				sb.Append( Environment.NewLine );
				sb.Append( "   '" );
				sb.Append( _results["LogToEventLog"] );
				sb.Append( "'" );
				sb.Append( Environment.NewLine );
			}
			sb.Append( Environment.NewLine );
		}
		if( _blnLogToFile )
		{
			sb.Append( " " );
			sb.Append( strBullet );
			sb.Append( " " );
			if( string.IsNullOrEmpty( _results["LogToFile"] ) )
			{
				sb.Append( "details were written to a text log at:" );
				sb.Append( Environment.NewLine );
				sb.Append( "   " );
				sb.Append( _strLogFilePath );
				sb.Append( Environment.NewLine );
			}
			else
			{
				sb.Append( "details could NOT be written to the text log due to an error:" );
				sb.Append( Environment.NewLine );
				sb.Append( "   '" );
				sb.Append( _results["LogToFile"] );
				sb.Append( "'" );
				sb.Append( Environment.NewLine );
			}
		}
		if( _blnLogToEmail )
		{
			sb.Append( " " );
			sb.Append( strBullet );
			sb.Append( " " );
			if( string.IsNullOrEmpty( _results["LogToEmail"] ) )
			{
				sb.Append( "an email was sent to: " );
				sb.Append( Config.GetString( "EmailTo", "" ) );
				sb.Append( Environment.NewLine );
			}
			else
			{
				sb.Append( "email could NOT be sent due to an error:" );
				sb.Append( Environment.NewLine );
				sb.Append( "   '" );
				sb.Append( _results["LogToEmail"] );
				sb.Append( "'" );
				sb.Append( Environment.NewLine );
			}
		}
		sb.Append( Environment.NewLine );
		sb.Append( Environment.NewLine );
		sb.Append( "Detailed error information follows:" );
		sb.Append( Environment.NewLine );
		sb.Append( Environment.NewLine );
		sb.Append( _exceptionText );

		return sb.ToString();
	}

	/// <summary>
	/// replace generic constants in display strings with specific values
	/// </summary>
	private string FormatDisplayString( string strOutput )
	{
		string strTemp = string.Empty;

		if( !string.IsNullOrEmpty( strOutput ) )
			strTemp = strOutput;

		strTemp = strTemp.Replace( "(app)", Config.GetString( "AppName", string.Empty ) );
		strTemp = strTemp.Replace( "(contact)", Config.GetString( "ContactInfo", string.Empty ) );
		return strTemp;
	}

	/// <summary>
	/// writes text plus newline to http response stream
	/// </summary>
	private void WriteLine( string line, params object[] args )
	{
		HttpContext.Current.Response.Write( string.Format( line, args ) );
		HttpContext.Current.Response.Write( Environment.NewLine );
	}

	private void WriteLine( string line )
	{
		HttpContext.Current.Response.Write( line );
		HttpContext.Current.Response.Write( Environment.NewLine );
	}

	/// <summary>
	/// writes current exception info to a web page
	/// </summary>
	private void ExceptionToPage()
	{
		string strWhatHappened = Config.GetString( "WhatHappened", "There was an unexpected error in this website. This may be due to a programming bug." );
		string strHowUserAffected = Config.GetString( "HowUserAffected", "The current page will not load." );
		string strWhatUserCanDo = Config.GetString( "WhatUserCanDo", "Close your browser, navigate back to this website, and try repeating your last action. Try alternative methods of performing the same action. If problems persist, contact (contact)." );

		string strPageHeader = Config.GetString( "PageHeader", "This website encountered an unexpected problem" );
		string strPageTitle = Config.GetString( "PageTitle", "Website problem" );

		HttpContext.Current.Response.Clear();
		HttpContext.Current.Response.ClearContent();

		try
		{
			// TODO: Make a template
			WriteLine( "<HTML><HEAD><TITLE>{0}</TITLE><STYLE>", FormatDisplayString( strPageTitle ) );
			WriteLine( "body {font-family:\"Verdana\";font-weight:normal;font-size: .7em;color:black; background-color:white;}" );
			WriteLine( "b {font-family:\"Verdana\";font-weight:bold;color:black;margin-top: -5px}" );
			WriteLine( "H1 { font-family:\"Verdana\";font-weight:normal;font-size:18pt;color:red }" );
			WriteLine( "H2 { font-family:\"Verdana\";font-weight:normal;font-size:14pt;color:maroon }" );
			WriteLine( "pre {font-family:\"Lucida Console\";font-size: .9em}" );
			WriteLine( "</STYLE></HEAD><BODY><H1>{0}<hr width=100% size=1 color=silver></H1><H2>What Happened:</H2><BLOCKQUOTE>{1}</BLOCKQUOTE>",
				FormatDisplayString( strPageHeader ),
				FormatDisplayString( strWhatHappened ) );
			WriteLine( "<H2>How this will affect you:</H2><BLOCKQUOTE>{0}</BLOCKQUOTE><H2>What you can do about it:</H2><BLOCKQUOTE>{1}</BLOCKQUOTE>",
				FormatDisplayString( strHowUserAffected ),
				FormatDisplayString( strWhatUserCanDo ) );
#if DEBUG
			// Show details if debug build
			WriteLine( "<INPUT type=button value=\"More Info &gt;&gt;\" onclick=\"this.style.display='none'; document.getElementById('MoreInfo').style.display='block'\">" );
			WriteLine( "<DIV style='display:none;' id='MoreInfo'><H2>More info:</H2><TABLE width=\"100%\" bgcolor=\"#ffffcc\"><TR><TD><CODE><PRE>" );
			WriteLine( FormatExceptionForUser() );
			WriteLine( "</PRE></CODE></TD></TR></TABLE></DIV>" );
#endif
			WriteLine( "</BODY></HTML>" );

		}
		catch( Exception ex )
		{
			HttpContext.Current.Response.Write( ex.ToString() );
		}

		HttpContext.Current.Response.Flush();
		// 
		// if( you use Server.ClearError to clear the exception in Application_Error, 
		// the defaultredirect setting in the Web.config file will not redirect the user 
		// because the unhandled exception no longer exists. if( you want the 
		// defaultredirect setting to properly redirect users, do not clear the exception 
		// in Application_Error.
		//
		HttpContext.Current.Server.ClearError();

		// don't let the calling page output any additional .Response HTML
		HttpContext.Current.Response.End();
	}

	/// <summary>
	/// returns current URL; "http://localhost:85/mypath/mypage.aspx?test=1&apples=bear"
	/// </summary>
	private string WebCurrentUrl()
	{
		string strUrl = string.Empty;

		NameValueCollection vars = HttpContext.Current.Request.ServerVariables;
		strUrl = "http://" + vars[ "server_name" ];

		if( vars[ "server_port" ] != "80" )
			strUrl += ":" + vars[ "server_port" ];

		strUrl += vars[ "url" ];

		if( !string.IsNullOrEmpty( vars[ "query_string" ] ) )
			strUrl += "?" + vars[ "query_string" ];

		return strUrl;
	}

	protected override string SysInfoToString()
	{
		return SysInfoToString( false );
	}

	/// <summary>
	/// gather some system information that is helpful in diagnosing exceptions
	/// </summary>
	private string SysInfoToString( bool blnIncludeStackTrace )
	{
		StringBuilder sb = new StringBuilder();

		sb.Append( "Date and Time:         " );
		sb.Append( DateTime.Now );
		sb.Append( Environment.NewLine );

		sb.Append( "Machine Name:          " );
		try
		{
			sb.Append( Environment.MachineName );
		}
		catch( Exception e )
		{
			sb.Append( e.Message );
		}
		sb.Append( Environment.NewLine );

		sb.Append( "Process User:          " );
		sb.Append( ProcessIdentity() );
		sb.Append( Environment.NewLine );

		sb.Append( "Remote User:           " );
		sb.Append( HttpContext.Current.Request.ServerVariables[ "REMOTE_USER" ] );
		sb.Append( Environment.NewLine );

		sb.Append( "Remote Address:        " );
		sb.Append( HttpContext.Current.Request.ServerVariables[ "REMOTE_ADDR" ] );
		sb.Append( Environment.NewLine );

		sb.Append( "Remote Host:           " );
		sb.Append( HttpContext.Current.Request.ServerVariables["REMOTE_HOST" ] );
		sb.Append( Environment.NewLine );

		sb.Append( "URL:                   " );
		sb.Append( WebCurrentUrl() );
		sb.Append( Environment.NewLine );
		sb.Append( Environment.NewLine );

		sb.Append( "NET Runtime version:   " );
		sb.Append( System.Environment.Version.ToString() );
		sb.Append( Environment.NewLine );

		sb.Append( "Application Domain:    " );
		try
		{
			sb.Append( System.AppDomain.CurrentDomain.FriendlyName );
		}
		catch( Exception e )
		{
			sb.Append( e.Message );
		}
		sb.Append( Environment.NewLine );

		if( blnIncludeStackTrace )
			sb.Append( EnhancedStackTrace() );

		return sb.ToString();
	}

	/// <summary>
	/// translate an exception object to a formatted string, with additional system info
	/// </summary>
	protected string ExceptionToString( Exception ex )
	{
		StringBuilder sb = new StringBuilder();

		sb.Append( ExceptionToString( ex, true ) );

		// get ASP specific settings
		try
		{
			sb.Append( GetASPSettings() );
		}
		catch( Exception e )
		{
			sb.Append( e.Message );
		}
		sb.Append( Environment.NewLine );

		return sb.ToString();
	}

	/// <summary>
	/// returns formatted string of all ASP.NET collections (QueryString, Form, Cookies, ServerVariables)
	/// </summary>
	private string GetASPSettings()
	{
		StringBuilder sb = new StringBuilder();

		const string strSuppressKeyPattern = "^ALL_HTTP|^ALL_RAW|VSDEBUGGER";

		sb.Append( "---- ASP.NET Collections ----" );
		sb.Append( Environment.NewLine );
		sb.Append( Environment.NewLine );
		sb.Append( HttpVarsToString( HttpContext.Current.Request.QueryString, "Query String" ) );
		sb.Append( HttpVarsToString( HttpContext.Current.Request.Form, "Form" ) );
		sb.Append( HttpVarsToString( HttpContext.Current.Request.Cookies ) );
		sb.Append( HttpVarsToString( HttpContext.Current.Session ) );
		sb.Append( HttpVarsToString( HttpContext.Current.Cache ) );
		sb.Append( HttpVarsToString( HttpContext.Current.Application ) );
		sb.Append( HttpVarsToString( HttpContext.Current.Request.ServerVariables, "Server Variables", true, strSuppressKeyPattern ) );

		return sb.ToString();
	}

	/// <summary>
	/// returns formatted string of all ASP.NET Cookies
	/// </summary>
	private string HttpVarsToString( HttpCookieCollection c )
	{
		if( c.Count == 0 ) return string.Empty;

		StringBuilder sb = new StringBuilder();

		sb.Append( "Cookies" );
		sb.Append( Environment.NewLine );
		sb.Append( Environment.NewLine );

		foreach( string key in c )
			AppendLine( sb, key, c[ key ].Value );

		sb.Append( Environment.NewLine );

		return sb.ToString();
	}

	/// <summary>
	/// returns formatted summary string of all ASP.NET app vars
	/// </summary>
	private string HttpVarsToString( HttpApplicationState a )
	{
		if( a.Count == 0 ) return string.Empty;

		StringBuilder sb = new StringBuilder();

		sb.Append( "Application" );
		sb.Append( Environment.NewLine );
		sb.Append( Environment.NewLine );

		foreach( string key in a )
			AppendLine( sb, key, a[ key ] );

		sb.Append( Environment.NewLine );
		return sb.ToString();
	}

	/// <summary>
	/// returns formatted summary string of all ASP.NET Cache vars
	/// </summary>
	private string HttpVarsToString( System.Web.Caching.Cache c )
	{
		if( c.Count == 0 ) return string.Empty;

		StringBuilder sb = new StringBuilder();

		sb.Append( "Cache" );
		sb.Append( Environment.NewLine );
		sb.Append( Environment.NewLine );

		foreach( System.Collections.DictionaryEntry de in c )
			AppendLine( sb, Convert.ToString( de.Key ), de.Value );

		sb.Append( Environment.NewLine );
		return sb.ToString();
	}

	/// <summary>
	/// returns formatted summary string of all ASP.NET Session vars
	/// </summary>
	private string HttpVarsToString( System.Web.SessionState.HttpSessionState s )
	{
		// sessions can be disabled
		if( s == null ) return string.Empty;
		if( s.Count == 0 ) return string.Empty;

		StringBuilder sb = new StringBuilder();

		sb.Append( "Session" );
		sb.Append( Environment.NewLine );
		sb.Append( Environment.NewLine );

		foreach( string key in s )
			AppendLine( sb, key, s[ key ] );

		sb.Append( Environment.NewLine );
		return sb.ToString();
	}

	private string HttpVarsToString( NameValueCollection nvc, string title )
	{
		return HttpVarsToString( nvc, title, false, string.Empty );
	}

	/// <summary>
	/// returns formatted string of an arbitrary ASP.NET NameValueCollection
	/// </summary>
	private string HttpVarsToString( NameValueCollection nvc, string title, bool suppressEmpty, string suppressKeyPattern )
	{
		if( !nvc.HasKeys() ) return string.Empty;

		StringBuilder sb = new StringBuilder();

		sb.Append( title );
		sb.Append( Environment.NewLine );
		sb.Append( Environment.NewLine );

		foreach( string key in nvc.Keys )
		{
			bool display = true;

			if( suppressEmpty )
				display = !string.IsNullOrEmpty( nvc[key] );

			if( key == _strViewstateKey )
			{
				_strViewstate = nvc[key];
				display = false;
			}

			if( display && !string.IsNullOrEmpty( suppressKeyPattern ) )
				display = !Regex.IsMatch( key, suppressKeyPattern );

			if( display )
				AppendLine( sb, key, nvc[key] );
		}

		sb.Append( Environment.NewLine );
		return sb.ToString();
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Andy Searls
Software Developer (Senior) Belami
United States United States
No Biography provided

You may also be interested in...

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.160426.1 | Last Updated 17 Mar 2008
Article Copyright 2008 by Andy Searls
Everything else Copyright © CodeProject, 1999-2016
Layout: fixed | fluid