Like any other component, a Web service should throw an unhandled exception to its client when it's unable to deal with the exception internally. However, two factors complicate this process. The first factor is that the client of the Web service might well be external to the organization running the service ”for instance, a client somewhere on the Internet. It's often not a good idea to throw detailed individual exceptions in these situations, because they can contain information that could be very useful to hackers trying to understand or break the Web service. The second factor is that when a SOAP client accesses a Web service,.NET will always translate any unhandled exception thrown by the Web service into a generic SoapException , thereby losing some of the information in the original exception. This can make debugging more difficult from the client point of view, so you need to consider how to make your Web service exceptions both easier to debug for authorized developers and harder to understand for unauthorized hackers.
If you click the Trigger raw exception button, you can see how the exception that's deliberately thrown by the Web service is perceived by the client application.
The resulting message box is shown in Figure 8-5. It shows quite clearly that the exception thrown by the Web service isn't the same as the one received by the client. Whereas the ThrowExceptionRaw Web method throws a NullReferenceException , the client in this case actually receives a SoapException . The SoapException message does show the text of the original exception, but that text is mixed with the standard "Server was unable to process request" exception message that you saw earlier in the Web browser. To try to extract the original exception details would be rather messy. The SoapFault field is correct, showing that the error occurred on the server, but the Web service URL is shown as blank.
The most interesting information is in the first line of the stack trace ”the exception was thrown by the SoapHttpClientProtocol.ReadResponse method. The client ”or to be more precise, the client proxy ”uses this method to read the SOAP response message sent by the Web service method.
The sequence of events is that after the Web service throws an unhandled exception, the HTTP Web service handler catches the exception and converts it into a SOAP response message. When the client proxy receives the SOAP response containing the exception information, it in turn converts that SOAP response into a SoapException . Finally, the client receives this SoapException . This mandatory process means that there's no way of throwing an exception from a Web service to its clients without having that exception converted into a generic SoapException . The message box in Figure 8-5 shows that this generic exception contains very little useful information. So the challenge is to devise a scheme that allows you to pass much more detailed and useful exception information from the Web service to its client.
Obviously, the information shown in Figure 8-5 is not adequate for easy understanding of what went wrong inside the Web service. It would be much better if you could provide a custom exception with exactly the information that you need to know, whatever that might be. At the most basic level, you probably want to be able to programmatically identify the following information about the exception:
The exact type of the original exception, without being forced to perform messy parsing of the SoapException.Message text
The original exception message, once again without being forced to perform messy parsing of the SoapException.Message text
The precise stack trace of the original exception
The Web service URL
The major issue to overcome is that any exception you wish to propagate out of a Web service has to be of type SoapException . So one effective way to provide the information is to catch the original exception and then create a new custom SoapException containing the details that you want to pass to the Web service client. Listing 8-4 shows the Web method ThrowExceptionCustom , which deliberately triggers an exception and then calls into another method to create a custom exception before throwing that back to the Web service client. This custom SoapException , therefore, replaces the generic SoapException that would otherwise be produced automatically by the Web service.
<WebMethod()> _ Public Sub ThrowExceptionCustom() Try 'This code will throw an exception Dim Test As Object Test.ToString() Catch Exc As Exception 'Substitute custom exception Throw CustomException(Exc) Finally 'Do cleanup here End Try End Sub
The real work is, of course, done within the private CustomException method, as shown in Listing 8-5. This function analyzes the original exception in order to build and return a custom SoapException containing the information that I mentioned previously as being the minimum required.
Private Function CustomException(ByVal Exc As Exception) As SoapException Dim doc As New System.Xml.XmlDocument 'Create detail node Dim DetailNode As System.Xml.XmlNode = _ doc.CreateNode(XmlNodeType.Element, _ SoapException.DetailElementName.Name, _ SoapException.DetailElementName.Namespace) 'Add original exception type Dim ExcType As System.Xml.XmlNode = _ doc.CreateNode(XmlNodeType.Element, _ "ExceptionType", _ SoapException.DetailElementName.Namespace) ExcType.InnerText = Exc.GetType.ToString 'Add original exception message Dim ExcMessage As System.Xml.XmlNode = _ doc.CreateNode(XmlNodeType.Element, _ "ExceptionMessage", _ SoapException.DetailElementName.Namespace) ExcMessage.InnerText = Exc.Message 'Add original exception stack trace Dim ExcStackTrace As System.Xml.XmlNode = _ doc.CreateNode(XmlNodeType.Element, _ "ExceptionTrace", _ SoapException.DetailElementName.Namespace) ExcStackTrace.InnerText = Exc.StackTrace 'Append the extra details to main detail node DetailNode.AppendChild(ExcType) DetailNode.AppendChild(ExcMessage) DetailNode.AppendChild(ExcStackTrace) 'Build and return new custom SoapException Return New SoapException("", SoapException.ServerFaultCode, _ Context.Request.Url.AbsoluteUri, DetailNode) End Function
The key to understanding the code in Listing 8-5 is that every SoapException can have a Detail XML node. By convention, this node contains application-specific error information. The CustomException method builds the Detail node and adds three child nodes, each containing an important piece of information from the original exception. The three items of information specified here are the original exception type, the original exception message, and the original exception stack trace. Once these child nodes have been created, they're added to the Detail node, and then the custom SoapException is built. The constructor that I'm using to create the new exception contains four arguments:
Message : This is left blank, because the Web service will populate it automatically before the exception reaches the client. You aren't really interested in this message because it doesn't provide useful information in an easily accessible way.
Code : This contains one of four predefined SOAP codes. Here it's being populated with the ServerFaultCode , signifying that the problem lies with the code inside the Web service.
Actor : By convention, this argument contains the URL of the Web service that caused the error.
Detail : This node contains the application-specific information that I'm interested in, namely the details that I grabbed from the original exception.
The code on the client side that catches the exception and displays it is shown in Listing 8-6. As you can see, it extracts the custom information that I specified by referring to each of the Detail child nodes by name. It also extracts the raw SOAP message for comparison and then displays the extracted information in a message box.
Private Sub TriggerCustomException_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles TriggerCustomException.Click Dim ExceptionMessage As String, Sep As String = Space$(4) 'Trigger custom exception and show it Try m_TimeTest.ThrowExceptionCustom() Catch SoapExc As System.Web.Services.Protocols.SoapException With SoapExc ExceptionMessage = "ORIGINAL EXCEPTION INFORMATION" ExceptionMessage += Environment.NewLine ExceptionMessage += "Type:" & Environment.NewLine ExceptionMessage += Sep & .Detail("ExceptionType").InnerText ExceptionMessage += Environment.NewLine ExceptionMessage += "Message:" & Environment.NewLine ExceptionMessage += Sep & .Detail("ExceptionMessage").InnerText ExceptionMessage += Environment.NewLine ExceptionMessage += "Stack trace:" & Environment.NewLine ExceptionMessage += Sep & .Detail("ExceptionTrace").InnerText ExceptionMessage += Environment.NewLine ExceptionMessage += "SOAP fault:" & Environment.NewLine ExceptionMessage += Sep & .Code.ToString ExceptionMessage += Environment.NewLine ExceptionMessage += "Web service URL:" & Environment.NewLine ExceptionMessage += Sep & .Actor.ToString ExceptionMessage += Environment.NewLine ExceptionMessage += "Raw SOAP message:" &; Environment.NewLine ExceptionMessage += Sep & .Message End With MessageBox.Show(ExceptionMessage, _ "Web service custom exception", _ MessageBoxButtons.OK) End Try End Sub
After all of this work, Figure 8-6 shows the message box as it displays the custom information that I added inside the Web service. If you contrast this information with that shown in Figure 8-5, you can see that a major improvement has taken place. Instead of having to parse the generic SoapException.Message in a messy way, all of the information is now readily available in individual child nodes.
Of course, you can adapt this code to add as many items of information as you wish to transfer, simply by creating an extra child node for each item.
If your Web service is running on the Internet, and is therefore subject to all of the security risks that come with that very public environment, you probably don't want to provide these detailed exception messages. They give away too much information to any hacker who's looking to probe or attack your Web service. It's far better in these circumstances to emit a bland , generic error message in the event of Web service failure. Listing 8-7 modifies the CustomException method shown previously in Listing 8-5 to check whether the client is on the same machine as the Web service. If not, it populates the exception Detail node with exception information that gives absolutely nothing away.
Private Function CustomException(ByVal Exc As Exception) As SoapException Dim doc As New System.Xml.XmlDocument 'Create detail node Dim DetailNode As System.Xml.XmlNode = _ doc.CreateNode(XmlNodeType.Element, _ SoapException.DetailElementName.Name, _ SoapException.DetailElementName.Namespace) 'Add original exception type Dim ExcType As System.Xml.XmlNode = _ doc.CreateNode(XmlNodeType.Element, _ "ExceptionType", _ SoapException.DetailElementName.Namespace) If Context.Request.UserHostAddress = "127.0.0.1" Then ExcType.InnerText = Exc.GetType.ToString Else ExcType.InnerText = "SoapException" End If 'Add original exception message Dim ExcMessage As System.Xml.XmlNode = _ doc.CreateNode(XmlNodeType.Element, _ "ExceptionMessage", _ SoapException.DetailElementName.Namespace) If Context.Request.UserHostAddress = "127.0.0.1" Then ExcMessage.InnerText = Exc.Message Else ExcMessage.InnerText = "Error - no details available" End If 'Add original exception stack trace Dim ExcStackTrace As System.Xml.XmlNode = _ doc.CreateNode(XmlNodeType.Element, _ "ExceptionTrace", _ SoapException.DetailElementName.Namespace) If Context.Request.UserHostAddress = "127.0.0.1" Then ExcStackTrace.InnerText = Exc.StackTrace Else ExcStackTrace.InnerText = "No stack trace available" End If 'Append the extra details to main detail node DetailNode.AppendChild(ExcType) DetailNode.AppendChild(ExcMessage) DetailNode.AppendChild(ExcStackTrace) 'Build and return new custom SoapException Return New SoapException("", SoapException.ServerFaultCode, _ Context.Request.Url.AbsoluteUri, DetailNode) End Function
Figure 8-7 shows the resulting message box displayed for a remote client. The custom information has been suppressed, and the Web service client is left with little information useful for hacking the Web service.
The one remaining piece of giveaway information is the raw message that the HTTP handler adds to the custom SOAP exception. This contains information about the exception's stack trace that is better to suppress. The key to removing this last clue lies in the CustomErrors section of Web.config. This setting has three values:
On : Display "friendly" (i.e., nondetailed) error information for all clients.
Off : Display detailed error information for all clients.
RemoteOnly : Display detailed error information for local clients and non-detailed error information for remote clients.
The best setting for most environments is RemoteOnly , which is also the default setting, as shown in Listing 8-8.
<system.web> <customErrors mode="RemoteOnly" /> </system.web>
This setting will remove the stack trace from the SOAP message for any nonlocal client. Note that you may need to reboot Visual Studio for this setting to take effect during Web service development.
You can normally catch unhandled exceptions in ASP .NET applications by adding code to the Application_Error event in the global.asax file. Unfortunately, this doesn't work for unhandled exceptions within a Web service. This is because the HTTP handler for XML Web services catches any unhandled exception and turns it into a SOAP fault before the Application_Error event is called. This SOAP fault then becomes a SoapException or a SoapHeaderException , depending on whether or not the exception was thrown while processing a SOAP header.
Using any of the other unhandled exception filters described in Chapter 13 also doesn't work for the same reason. So either you have to add exception handling to every single Web method or you create a SOAP extension, as described in the next few sections, that's bound to every Web method. The SOAP extension route is attractive because you don't have to add error handling to every Web method and you can write custom exception information directly into the SOAP response stream. The problem with this approach is that you no longer have access to the original exception or its context, which makes it rather difficult to create a custom exception with meaningful information. For this reason, adding error handling to every Web method is the recommended solution.