Introduction
This article is a follow-up to my previous CodeProject article, User Friendly Exception Handling, which covered global exception handling for console and WinForms apps. As I mentioned in that article:
This leaves out one large class of .NET applications: web apps. I've experimented with this, and I do not believe it is desirable to handle web exceptions using the exact same classes that you use to handle Console and WinForms exceptions. They have different needs. I do have a variant of this class I use in server-side Web Services and ASP.NET applications, but it's significantly different.
This article covers a similar global exception handling technique for ASP.NET applications and Web Services. I won't go over any of the background on exceptions, as that was covered in the introduction to the previous article. Let's jump right into the implementation.
Unhandled Exceptions in ASP.NET Websites
There are two approaches you can use when you set out to design a global exception handler in ASP.NET.
The first method is the most straightforward: using the Application_Error
event in the Global.asax file. This event fires whenever an unhandled exception occurs in an ASP.NET application. So, we can hook up our shared AspUnhandledException
class there:
Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
Dim ueh As New ASPUnhandledException.Handler
ueh.HandleException(Server.GetLastError.GetBaseException())
End Sub
Most ASP.NET developers are familiar with global.asax, so this method is easy to explain. It's also the method I used in the previous version of this article. While you can still do this, there is a better way.
And that second, better way is to hook into the ASP.NET HTTP pipeline by implementing IHttpModule
:
Public Class UehHttpModule
Implements IHttpModule
Public Sub Init(ByVal Application As System.Web.HttpApplication)
Implements System.Web.IHttpModule.Init
AddHandler Application.Error, AddressOf OnError
End Sub
Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
End Sub
Protected Overridable Sub OnError(ByVal sender As Object,
ByVal args As EventArgs)
Dim app As HttpApplication = CType(sender, HttpApplication)
Dim ueh As New ASPUnhandledException.Handler
ueh.Handle(app.Server.GetLastError)
End Sub
End Class
This is functionally identical to the first method, with one key difference: you can now implement global exception handling without recompiling the ASP.NET application! This means you can retrofit unhandled exception handling to any ASP.NET website with ease -- just copy the ASPUnhandledException.dll file to the \bin folder, then make a minor edit to the Web.config:
<system.web>
<httpModules>
<add name="UehHttpModule"
type="ASPUnhandledException.UehHttpModule, ASPUnhandledException" />
</httpModules>
</system.web>
And off you go. Isn't that cool?
Unhandled Exceptions in ASP.NET Web Services
Unfortunately, neither of these methods will work for a .NET Web Service. Application_Error
will never fire, and the HTTP pipeline is bypassed in favor of the SOAP pipeline. To globally handle exceptions for a web service, we have to implement a SoapExtension
:
Public Class UehSoapExtension
Inherits SoapExtension
Private _OldStream As Stream
Private _NewStream As Stream
Public Overloads Overrides Function GetInitializer( _
ByVal serviceType As System.Type) As Object
Return Nothing
End Function
Public Overloads Overrides Function GetInitializer( _
ByVal methodInfo As System.Web.Services.Protocols.LogicalMethodInfo, _
ByVal attribute As System.Web.Services.Protocols.SoapExtensionAttribute)
As Object
Return Nothing
End Function
Public Overrides Sub Initialize(ByVal initializer As Object)
End Sub
Public Overrides Function ChainStream(ByVal stream As Stream) As Stream
_OldStream = stream
_NewStream = New MemoryStream
Return _NewStream
End Function
Private Sub Copy(ByVal fromStream As Stream, ByVal toStream As Stream)
Dim sr As New StreamReader(fromStream)
Dim sw As New StreamWriter(toStream)
sw.Write(sr.ReadToEnd())
sw.Flush()
End Sub
Public Overrides Sub ProcessMessage(ByVal message _
As System.Web.Services.Protocols.SoapMessage)
Select Case message.Stage
Case SoapMessageStage.BeforeDeserialize
Copy(_OldStream, _NewStream)
_NewStream.Position = 0
Case SoapMessageStage.AfterSerialize
If Not message.Exception Is Nothing Then
Dim ueh As New Handler
Dim strDetailNode As String
strDetailNode = ueh.HandleWebServiceException(message)
_NewStream.Position = 0
Dim tr As TextReader = New StreamReader(_NewStream)
Dim s As String = tr.ReadToEnd
s = s.Replace("<detail />", strDetailNode)
_NewStream = New MemoryStream
Dim tw As TextWriter = New StreamWriter(_NewStream)
tw.Write(s)
tw.Flush()
End If
_NewStream.Position = 0
Copy(_NewStream, _OldStream)
End Select
End Sub
End Class
The SoapExtension
is a little more complicated than our HttpModule
, because we have to modify the SOAP message to include the detailed exception information. I'll cover this in more detail later. All you need to do to get this working in your Web Service is copy the ASPUnhandledException.dll file to the \bin folder, then make a minor edit to the Web.config:
<webServices>
<soapExtensionTypes>
<add type="ASPUnhandledException.UehSoapExtension, ASPUnhandledException"
priority="1" group="0" />
</soapExtensionTypes>
</webServices>
Configuring AspUnhandledException
The AspUnhandledException
class is configured through a custom configuration section named <UnhandledException>
in Web.config. The class will work without any configuration, but all you'll get by default is a plain text log file in the root of your website. I suggest adding the following to your Web.config as a practical minimum:
<configSections>
<section name="UnhandledException"
type="System.Configuration.NameValueSectionHandler, System,
Version=1.0.5000.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089" />
</configSections>
<UnhandledException>
<add key="ContactInfo" value="Ima Testguy at 123-555-1212" />
<add key="EmailTo" value="me@mydomain.com" />
<add key="SmtpDefaultDomain" value="mydomain.com" />
<add key="SmtpServer" value="mail.mydomain.com" />
</UnhandledException>
This is the complete list of configuration settings:
Key |
Default |
Description |
EmailTo |
none |
semicolon delimited list of email addresses to send exception notifications to |
EmailFrom |
server@domain.com |
override for the from address provided in exception emails. Optional. |
IgnoreDebug |
True |
do not handle any exceptions when the debugger is attached, or if the originating exception host is localhost. If you want to debug the error handler, be sure to set this to False . |
IgnoreRegex |
"" |
If this regular expression pattern evaluates to true against any part of the exception string, ignore this exception. |
LogToEventLog |
False |
write an unhandled exception event to the Event Log |
LogToEmail |
True |
send an unhandled exception email to the addresses in EmailTo |
LogToFile |
True |
write an unhandled exception entry to a plain text log file |
LogToUI |
True |
automatically display a customized unhandled exception HTML page |
PathLogFile |
"" |
path to the plain text exception log file. Can be fully qualified or relative, with or without a filename. If it is relative, assumed to be relative to the root folder of the website. If no path provided, the default filename is used at the root of the website. |
AppName |
none |
used on default error HTML page to replace any instances of "(app)" in strings |
ContactInfo |
none |
used on default error HTML page to replace any instances of "(contact)" in strings |
PageTitle |
(see screenshot) |
title of default error HTML page |
PageHeader |
(see screenshot) |
header of default error HTML page |
WhatHappened |
(see screenshot) |
Text to display in default error HTML web page under the "What happened:" heading |
HowUserAffected |
(see screenshot) |
Text to display in default error HTML web page under the "How this will affect you:" heading |
WhatUserCanDo |
(see screenshot) |
Text to display in default error HTML web page under the "What you can do about it:" heading |
Once you've copied the .dll file to the /bin folder, and checked your Web.config settings, everything else is automatic from this point on.
ASPUnhandledException with ASP.NET websites
Okay, so now that you've hooked this up, what do you get for your troubles? Well, whenever an unhandled exception occurs in your website or web service, one or more of the following will automatically happen:
- A user friendly webpage is displayed to the user
- An email notification is sent to the address(es) of your choice
- A plaintext log file is written to the path and filename of your choice
- An entry is written to the event log
All of these options will contain the same detailed diagnostic information about the exception, which is generated by ExceptionToString
. I'll now cover each of these events in more detail.
If LogToUI
is left at the default of True
, we automatically generate a user-friendly web page based on the GUI design laid out by Alan Cooper in his book About Face: The Essentials of User Interface Design, in the chapter titled The End of Errors. Note that if you turn off LogToUI
, you'll revert to the default ASP.NET "yellow screen of death" page.
If LogToEmail
is left at the default of True
, your unhandled exceptions will automatically be emailed via the bundled managed SMTP class SimpleMail
. This class requires valid SMTP settings in the Web.config custom configuration section <UnhandledException>
. The complete list of configuration settings is:
Key |
Default |
Description |
SmtpDefaultDomain |
none |
Default email domain name used for emails that don't provide a domain |
SmtpServer |
none |
SMTP email server used to send emails |
SmtpPort |
25 |
SMTP port used to send emails |
SmtpAuthUser |
none |
If outgoing mail authentication is enabled, user name used for authentication |
SmtpAuthPassword |
none |
If outgoing mail authentication is enabled, password used for authentication |
The exception email contains:
- User and machine identification
- Application information
- Exception summary
- Custom stack trace
- All ASP.NET collection contents
If LogToFile
option is left at the default of True
, unhandled exceptions will be written to a plain text log file in the root of your website, named UnhandledExceptionLog.txt. If you require a different path, use the PathLogFile
setting to specify either a relative or absolute path, with or without a filename. If the path is relative, it is assumed to be relative to the root of the current website. Whatever path you specify must have ASP.NET write permissions, otherwise the logging will silently fail.
If LogToEventLog
is enabled, you will get this same detailed exception information in the Application Event Log. Note that you must grant the ASP.NET process account permission to write to the registry for this to function. I don't frequently do this in my apps, which is why I defaulted it to False
, but it does work!
ASPUnhandledException with ASP.NET Web Services
In my previous article, I bemoaned the lack of good global error handling provisions for ASP.NET Web Services. Well, not any more. Using a custom SoapExtension
, it's easy! And generally, it works the same as in ASP.NET. There are, however, a few important differences you should be aware of:
- Using a
SOAPExtension
means we only catch SOAP unhandled exceptions-- you will not be able to test unhandled exceptions using the web browser interface! Please bear this in mind! I included a demo SOAP console application in the solution for this very reason.
- SOAP clients don't have a browser interface, so the
LogToUI
option is not available for Web Services, and is ignored in this case.
- The
SoapException
is a bit different than your typical .NET Exception, it contains a special XML <detail>
element for details on the server part of the exception.
The only tricky part left is inserting the rich exception information we've come to expect from our ASP.NET exceptions into the SOAP message. By default, the <detail>
element is very sparse, so almost nothing is communicated from the server to the client:
<soap:Fault>
<faultcode>soap:Server</faultcode>
<faultstring>SoapException</faultstring>
<detail/>
</soap:Fault>
Hard to diagnose this exception with so little information. That's where the Handler.HandleWebServiceException
comes in. We call this method from the SoapExtension
, and it generates a much better <detail>
element for us:
Public Function HandleWebServiceException(ByVal sm As _
System.Web.Services.Protocols.SoapMessage) As String
_blnLogToUI = False
HandleException(sm.Exception)
Dim doc As New Xml.XmlDocument
Dim DetailNode As Xml.XmlNode = doc.CreateNode(XmlNodeType.Element, _
SoapException.DetailElementName.Name, _
SoapException.DetailElementName.Namespace)
Dim TypeNode As Xml.XmlNode = doc.CreateNode(XmlNodeType.Element, _
"ExceptionType", _
SoapException.DetailElementName.Namespace)
TypeNode.InnerText = _strExceptionType
DetailNode.AppendChild(TypeNode)
Dim MessageNode As Xml.XmlNode = doc.CreateNode(XmlNodeType.Element, _
"ExceptionMessage", _
SoapException.DetailElementName.Namespace)
MessageNode.InnerText = sm.Exception.Message
DetailNode.AppendChild(MessageNode)
Dim InfoNode As Xml.XmlNode = doc.CreateNode(XmlNodeType.Element, _
"ExceptionInfo", _
SoapException.DetailElementName.Namespace)
InfoNode.InnerText = _strException
DetailNode.AppendChild(InfoNode)
Return DetailNode.OuterXml.ToString()
End Function
The string returned from this method is used to modify the SOAP message "in flight", and we get a much better <detail>
node:
<soap:Fault>
<faultcode>soap:Server</faultcode>
<faultstring>Server was unable to process request. -->
I trimmed many of the <ExceptionInfo>
lines, but it's the same rich diagnostic information you've seen before.
Conclusion
One final observation on Unhandled Exceptions, before we close our discussion. You must avoid exceptions in the Unhandled Exception Handler. This is a special case handler, the "handler of last resort", and exceptions in this code are extremely bad form. They can cause the code to terminate with no warning whatsoever, as if you put in an arbitrary Return
right smack dab in the middle of your function. I have taken great precautions in my handler to avoid exceptions, but be warned.
I've used this class on about a dozen different websites and web services so far, with excellent results. There are many more details and comments in the source code provided at the top of the article, so check it out. Please don't hesitate to provide feedback, good or bad!
I hope you enjoyed this article. If you did, you may also like my other articles as well.
History
- Saturday, August 21, 2004 - Published
- Monday, September 27, 2004 - Updated demo code
- Fixed problem where inner exceptions were discarded in global.asax.
- Added code to ignore the outermost ASP.NET exception (it's the same every time).
- Removed
ConfigurationException
that was previously thrown when Company and Product were not populated in AssemblyInfo.
- Sunday, October 17, 2004 - Version 2.0
- Added
SoapExtension
and HttpHandler
.
- Many improvements to base Handler class (and a few bug fixes).
- Rebuilt article and demo solution.
- Saturday, December 18, 2004 - Version 2.1
- Uses a separate custom
<UnhandledException>
.config file section.
- added .config values
EmailFrom
, PageTitle
and PageHeader
.
- viewstate is sent as a plaintext email attachment (when present).
- smarter, more concise formatting of ASP.NET collections.
- added ASP.NET Session, Cache, and Application collection summary.
- more feedback provided in the
LogToUi
page when there is a failure to log via email, file, or event log.
- the "More Details:" section of the HTML error page is now an expander button to simplify the output for casual users.
- converted to VB.NET 2005 style XML comments.