Using SOAP Faults

 

Scott Seely
Microsoft Corporation

September 20, 2002

Summary: Scott Seely shows how to use SOAP Faults to deliver the appropriate level of detail to the developer at development time, and to the customer while the Web service is in production. (14 printed pages)

Introduction

In development, when an error is thrown, you will want to know where the error originated. Because this information is not useful to consumers of the Web service, you won't want to return meaningless line numbers when the service is deployed. Instead, you will want to provide other contextual information about what happened. In this column, I want to take a look at how to use SOAP Faults to deliver the right levels of detail to you at development time and to your customers while the Web service is in production. In case you are unfamiliar with the basic layout of a SOAP Fault, let's first take a quick look at the SOAP Fault element.

The SOAP Fault element has four separate pieces:

  • faultcode: Contains a value of VersionMismatch, MustUnderstand, Client, or Server. We'll look at how each of these items is set later in the column.
  • faultstring: Provides a human-readable explanation of why the Fault occurred.
  • faultactor: Indicates the URI associated with the actor that caused the Fault on the message path. In RPC-style messaging, the actor should be the URI of the invoked Web service.
  • detail: Carries information about why the error happened. This element may contain more XML elements or it might just be plain text.

In general, a Fault is analogous to an application exception. All SOAP toolkits that I am aware of convert a returned SOAP Fault into an exception (.NET, various Java stacks) or an error code (Microsoft SOAP Toolkit, SOAP::Lite). Why do I mention this? When your Web service returns a Fault, the Fault will be presented as an exception to the calling code. When returning a Fault, it is vital to know your audience; the SOAP Fault should offer meaningful information.

What the phrase "meaningful information" means, however, depends on your context. If you are the developer of a Web service and something bad happens, meaningful information might mean, for instance, the exact location of where the exception happened within your code and the associated call stack. If you are consuming the Web service, you will be very interested in why your input is invalid, but you may not care what server-side code failed.

To generate meaningful faults, we will look at the following related topics:

  • Setting the appropriate faultcode.
  • Data returned in Fault/faultmessage.
  • Leaking application exceptions.
  • Using the detail element.

We will take a look at all of this with respect to Microsoft® ASP.NET Web services. As such, we will be focusing pretty heavily on two classes: System.Web.Services.Protocols.SoapException and System.Web.Services.Protocols.SoapHeaderException. These two classes work the same. The only difference: if an error is found when processing a header, you must use SoapHeaderException. If the error is in the body, use SoapException. This discussion will focus on using SoapException. Let's start this discussion out by looking at setting the faultmessage.

Fault/faultmessage

Whenever the code within a Web service raises an exception, ASP.NET catches that exception and transforms it into a SOAP Fault. For example, let's assume that under a certain set of circumstances, the following line will be executed before the Web service can respond to the client:

Throw New Exception("Something bad happened") 

When this code is executed, the ASP.NET ASMX handler will catch the exception. If you generated the Web service project using Microsoft Visual Studio® .NET and left all the default settings alone, the returned Fault will look like this:

<soap:Fault>
   <faultcode>soap:Server</faultcode>
   <faultstring>System.Web.Services.Protocols.SoapException: Server was 
unable to process request. ---&gt; System.Exception: Something bad 
happened at AYS17Sept2002.Service1.CallFault() in 
c:\inetpub\wwwroot\AYS17Sept2002\Service1.asmx.vb:line 49
   --- End of inner exception stack trace ---</faultstring>
   <detail />
</soap:Fault>

This type of information is great for when you are developing the Web service and writing clients to exercise the code. It pinpoints where you have made mistakes and lets you know where you need to add items such as parameter validation. At development time, this is great. This feature is not so great when you deploy the Web service. When clients begin using the Web service, you do not want to be returning things such as what function failed in which module. This reveals information about the structure of the code and may aid an attacker in breaking your Web service.

So what do you do when you deploy the Web service? You need to open up Web.config and edit the /configuration/system.web/customErrors/@mode attribute. By default, this attribute is set to RemoteOnly. The mode attribute accepts two other values as well:

  • On
  • Off

The On setting tells ASP.NET not to add the stack trace information to any Faults. RemoteOnly causes the extra information to show up for clients on the same server and hides the information for remote users. When set to Off, the extra information shows up for all calls to the Web service. This is the same behavior you will see for Web pages. By setting the mode attribute to On and throwing the same exception, I will get the following Fault instead:

<soap:Fault>
    <faultcode>soap:Server</faultcode>    
    <faultstring>Server was unable to process request. --&gt; Something 
bad happened</faultstring>
    <detail />
</soap:Fault>

At this point, I am returning the same Fault without any information about where the exception was generated. I like that because I'm not revealing the structure of my code to anyone. By returning whatever exception is generated by the internal code, I still might reveal interesting tidbits to the caller. For example, if the Web service accesses a database, many of the exceptions could reveal the user identity accessing the database as well as the table structure. Knowing the table structure, an attacker might try to wreak some havoc. Average users may not care that an overflow occurred or that the database could not be accessed. They would probably be happier with a return value that simply says the server could not process the request. For that, you can use the SoapException class. This class gets special handling and can hide more information that the client does not care about (and often should not know about). Consider a function such as the following:

<WebMethod()> _
Public Sub CallFault()
    Try
        ' Deliberately throw an exception here
        Throw New Exception("Throwing a fault at " + _
            DateTime.Now.ToString())
    Catch ex As Exception
        Throw New SoapException( _
            "Server was unable to process request.", _
            SoapException.ServerFaultCode)
    End Try
End Sub

The above code always throws an exception. It could just as easily be accessing a database, reading a file, or grabbing data over the network. The point here is that the Web service, as written, does not tell the caller about internally generated exceptions. Instead, the service tells the client that something bad happened and returns. Because one Exception is caught and a SoapException is thrown, the faultstring is reduced to:

<faultstring>Server was unable to process request.</faultstring>

While hiding the reason why the server could not process the request is a good idea (you won't reveal information about the internal structure of your application), you will probably want to store the actual reason why the Fault was thrown in some location. Places to log problems include a database, event logs, and the Web application trace. Of these three, only one will be available to your application all the time: the application trace. If the database is unreachable due to network/security issues, or the application does not have the right permissions to log to an event log, you may never capture the exception. The application trace has its own problems. In particular, it only holds a finite number of trace statements and those statements may disappear if the Web application is reset or cycled for any reason. As a result, I recommend logging the messages in two locations: a database table or event log, and the application trace.

To make CallFault use the application trace, I edited it to look like this:

<WebMethod()> _
Public Sub CallFault()
    Try
        ' Deliberately throw an exception here
        Throw New Exception("Throwing a fault at " + _
            DateTime.Now.ToString())
    Catch ex As Exception
        Me.Context.Trace.Write(ex.ToString())
        Throw New SoapException( _
            "Server was unable to process request.", _
            SoapException.ServerFaultCode)
    End Try
End Sub

For the trace to work, the /configuration/system.web/trace element in Web.config must have the enabled attribute set to true. I recommend leaving the remaining items alone. For a heavily used Web service, you may consider raising the value of the requestLimit attribute as well so that the trace can keep more values around. This particular Web service is deployed in a Web application called AYS17Sept2002. To view the trace messages, I navigate to the URL, https://localhost/AYS17Sept2002/Trace.axd. After running an application that calls the Web service 10 times, the application trace has 10 entries. Selecting one entry, I get lots of information about what happened during the execution of the Web service. The trace section is of particular interest to me. It contains the following information:

Figure 1. The trace information for the captured exception

That pretty much sums up how to use faultmessage and how to use the element from the server. The next item of interest is looking at the faultcode. Not all problems happen at the server, but the server can detect a number of them. For these situations, it is really important to state who caused the problem so that it can be fixed.

Fault/faultcode

Lots of things can go wrong with a Web service message. The server may encounter a problem, input data may be wrong, or a header may come across that the server does not understand. The faultcode can have any one of the following values:

  • VersionMismatch: The SOAP receiver saw a namespace associated with the SOAP envelope that it does not recognize. When this faultcode is received, the message should not be resent. The SOAP namespace needs to be set to something the receiver does understand.
  • MustUnderstand: An immediate child of the SOAP header had mustUnderstand set to true. The receiver of the message did not understand the header. The receiver will need to be updated somehow (new code, new libraries, and so on) in order to make sense of the header.
  • Client: Something about the way that the message was formatted or the data it contained was wrong. The client needs to fix its mistake in order for the message to be sent back. When returning this faultcode, you should also fill in the details element with some specifics on what needs to happen in order for the message to go through.
  • Server: An error happened at the server. Depending on the nature of the error, you may be able to resend the exact same message to the server and see it processed. Of course, the server itself may need an update. For example, if a database connection string is incorrect, no amount of resending the message will result in success.

When any exception other than System.Web.Services.Protocols.SoapException is thrown from the code executed by the server side of the Web service, ASP.NET will set the faultcode to Server. Of the above faults, you will typically allow ASP.NET to handle inspecting the message for the VersionMismatch and MustUnderstand faultcodes. VersionMismatch is caught whenever the SOAP Envelope namespace does not match up with what the environment expects. A MustUnderstand Fault will be sent whenever a Header child element comes along that has the mustUnderstand attribute set to either true or 1 and it is not understood. In most WebMethod-based Web services, the header is handled by the SoapHeaderAttribute. This attribute maps a public element of the Web service class to an incoming header child element. When a header child element comes along, ASP.NET will look for the SoapHeaderAttribute attribute on the target method. If none is found for the particular mapping and mustUnderstand is true or 1, the Web service will return a MustUnderstand faultcode.

All the faultcode values are held in the SoapException class as static class members. They are:

  • VersionMismatchFaultCode
  • MustUnderstandFaultCode
  • ClientFaultCode
  • ServerFaultCode

When writing a Web service that throws a SoapException, you should really only throw Faults that use the ClientFaultCode or ServerFaultCode. The other two will normally be handled and thrown by ASP.NET. In general, you will want to use the ClientFaultCode for errors detected in the data sent to you. The values may be invalid or missing. The user may also pass in a value meant to lookup a particular record. If the value does not reference an existing item, you may also choose to return a Fault. Keep in mind that it may be appropriate to return nothing. Let's look how you might do validation for a Web service that takes some basic user information. The class it is taking in looks like this:

Public Class PersonData
    Public FirstName As String
    Public LastName As String
    Public EmailAddress As String
End Class

While it would be best to perform validation in PersonData by adding custom properties, this contrived example will do the validation in the Web service code. Using regular expression evaluation, the data is verified and an appropriate Fault is returned.

<WebMethod()> _
Public Sub StoreData(ByVal pd As PersonData)

    ' Check names using regular expressions
    Dim nameRegEx As New Regex("^([A-Z])([a-z])+")
    Dim emailRegEx As New Regex("^([a-zA-Z0-9_\-\.]+)@" & _
        "((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([" & _
        "a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$")
    If Not (nameRegEx.IsMatch(pd.FirstName)) Then
        Throw New SoapException("Invalid First Name", _
            SoapException.ClientFaultCode)
    ElseIf Not (nameRegEx.IsMatch(pd.LastName)) Then
        Throw New SoapException("Invalid Last Name", _
            SoapException.ClientFaultCode)
    ElseIf Not (emailRegEx.IsMatch(pd.EmailAddress)) Then
        Throw New SoapException("Invalid e-mail address", _
            SoapException.ClientFaultCode)
    End If

    ' Do something to store the data
End Sub

By the way, I did not cook up the e-mail regular expression on my own. I found this one at http://www.regxlib.com/Default.aspx. It handles many special cases I didn't think of when I first tried to create the expression on my own.

This validation code reflects how you would want to create your own parameter validation for your own Web service. When returning a Fault, you may also want to give extra details. To do that, you use the Fault/detail element.

Fault/detail

When you have a client Fault, you should explain to the client why the Fault occurred and how they could fix their own code. To add this information, the Fault has an optional detail element. This element can contain any valid XML and can only be used when the Fault is describing something that happened in the SOAP body. This is the only element that cannot be set using both the SoapException and SoapHeaderException class. This includes plain old text. To show how to use this element, I thought it might be interesting to validate all the elements in the PersonData class and return a Fault if any elements are invalid. This will return information on all invalid elements. To handle this, I created an enumeration that can be used to identify the element that is invalid.

Public Enum PersonDataItem
    FirstName
    LastName
    EmailAddress
End Enum

Then I created a small class that I can use to explain what is wrong with the element. Since I use regular expressions to validate the input, why not simply send a regular expression to show the right format?

Public Class PersonErrorInfo
    Public ItemInError As PersonDataItem
    Public CorrectRegularExpression As String
End Class

Why did I create a special class? I want to be able to use the XmlSerializer class and its ability to automatically serialize the class. The error-checking code would now look something like this:

<WebMethod()> _
Public Sub StoreData(ByVal pd As PersonData)

    ' Check names using regular expressions
    Dim nameRegEx As New Regex("^([A-Z])([a-z])+")
    Dim emailRegEx As New Regex("^([a-zA-Z0-9_\-\.]+)@" & _
        "((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([" & _
        "a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$")
    Dim errorDetails(3) As PersonErrorInfo
    Dim errorIndex As Integer = 0

    ' Check the information and see if it is valid.
    ' If not, add an entry to the errorDetails array.
    If Not (nameRegEx.IsMatch(pd.FirstName)) Then
        errorDetails(errorIndex) = New PersonErrorInfo()
        errorDetails(errorIndex).ItemInError = PersonDataItem.FirstName
        errorDetails(errorIndex).CorrectRegularExpression = _
            nameRegEx.ToString()
        errorIndex = errorIndex + 1
    End If
    If Not (nameRegEx.IsMatch(pd.LastName)) Then
        errorDetails(errorIndex) = New PersonErrorInfo()
        errorDetails(errorIndex).ItemInError = PersonDataItem.LastName
        errorDetails(errorIndex).CorrectRegularExpression = _
            nameRegEx.ToString()
        errorIndex = errorIndex + 1
    End If
    If Not (emailRegEx.IsMatch(pd.EmailAddress)) Then
        errorDetails(errorIndex) = New PersonErrorInfo()
        errorDetails(errorIndex).ItemInError = _
            PersonDataItem.EmailAddress
        errorDetails(errorIndex).CorrectRegularExpression = _
            emailRegEx.ToString()
        errorIndex = errorIndex + 1
    End If

    ' If we found an error, prepare and send the SOAP Fault.
    If (errorIndex > 0) Then

        ' Set up the serialization items
        Dim ser As New XmlSerializer(GetType(PersonErrorInfo))
        Dim stm As New MemoryStream()
        Dim utfEnc As New UTF8Encoding()
        Dim xmlw As New XmlTextWriter(stm, System.Text.Encoding.UTF8)
        Dim index As Integer

        ' Write the start element to the stream
        xmlw.WriteStartElement("detail")

        ' Create the serialization of the details
        For index = 0 To errorIndex - 1
            ser.Serialize(xmlw, errorDetails(index))
        Next

        ' Close the last tag and flush the contents
        xmlw.WriteEndElement()
        xmlw.Flush()

        ' Create a new document to write the stream to.
        Dim doc As New XmlDocument()
        Dim xmlStr As String = utfEnc.GetString(stm.GetBuffer())

        ' The string contains a byte string that indicates the
        ' byte order. Delete it right away.
        xmlStr = xmlStr.Substring(1)
        System.Diagnostics.Trace.WriteLine(xmlStr)
        doc.LoadXml(xmlStr)

        ' Were done with the stream, so close it.
        xmlw.Close()

        ' Send the fault
        Throw New SoapException("Invalid input", _
            SoapException.ClientFaultCode, _
            Context.Request.Url.ToString(), _
            doc.DocumentElement)
    End If

End Sub

If all the elements have something wrong, the output message would look like this:

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope 
    xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <soap:Fault>
      <faultcode>soap:Client</faultcode>
      <faultstring>Invalid input</faultstring>
      <faultactor
      >http://sseely2/AYS17Sept2002/Service1.asmx</faultactor>
      <detail>
        <PersonErrorInfo 
            xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
          <ItemInError>FirstName</ItemInError>
          <CorrectRegularExpression
          >^([A-Z])([a-z])+</CorrectRegularExpression>
        </PersonErrorInfo>
        <PersonErrorInfo 
            xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
          <ItemInError>LastName</ItemInError>
          <CorrectRegularExpression
          >^([A-Z])([a-z])+</CorrectRegularExpression>
        </PersonErrorInfo>
        <PersonErrorInfo 
            xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
          <ItemInError>EmailAddress</ItemInError>
          <CorrectRegularExpression
          >^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-
9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})
(\]?)$</CorrectRegularExpression>
        </PersonErrorInfo>
      </detail>
    </soap:Fault>
  </soap:Body>
</soap:Envelope>

That's pretty much all you need to know about using a special detail element. For a client that receives such a Fault with a custom detail element, that client can walk the detail element or serialize it to an internal type. Here is some sample code that generates a slightly more readable version of the Fault for users who do not like to read XML.

First, we have a function that calls the StoreData Web service:

Sub CallStoreData()
    Dim svc As New localhost.Service1()
    Console.WriteLine("Calling StoreData")
    Try
        Dim pd As New localhost.PersonData()
        pd.EmailAddress = "sseely@microsoft.com"

        ' The following line should generate an error.
        ' The first letter of each name must be 
        ' capitalized.
        pd.FirstName = "scott"
        pd.LastName = "Seely"
        svc.StoreData(pd)
    Catch ex As SoapException
        DisplayNode(0, ex.Detail)
    Catch ex As Exception
        Console.WriteLine("Unexpected exception: " & ex.ToString())
    Finally
        If Not (svc Is Nothing) Then
            svc.Dispose()
        End If
    End Try
End Sub

When a SoapException is received, the code calls DisplayNode to show the detail element in a more readable fashion:

Sub DisplayNode(ByVal displayDepth As Integer, _
    ByVal theNode As XmlNode)
    Dim indentString As String = _
        New String(ChrW(32), 3 * displayDepth)
    Dim elem As XmlNode

    ' Don't display whitespace nodes.
    If (theNode.Name <> "#whitespace") Then
        Console.WriteLine(indentString & theNode.Name & _
            ": " & theNode.Value)
    End If

    ' Display the child nodes
    For Each elem In theNode.ChildNodes
        DisplayNode(displayDepth + 1, elem)
    Next
End Sub

Because the client code to StoreData intentionally pushes out invalid information, the error information is displayed. When I run the client against the Web service, I get the following output:

Calling StoreData
detail:
   PersonErrorInfo:
      ItemInError:
         #text: FirstName
      CorrectRegularExpression:
         #text: ^([A-Z])([a-z])+

Okay, so that isn't super easy to understand, but for many people it may be easier to read than the straight XML. At any rate, you can comb over the returned detail element and create something that is meaningful for the computer or any humans that happen to be using the computer.

Summary

As a developer of Web services, you need to focus on how any error information is returned to clients. Things that are helpful to you as a developer can reveal details about your application that you may not want to display once the Web service is deployed. To handle visibility, you need to capture exceptions before they are handled by ASP.NET so that details are not leaked out inadvertently. The visibility is handled by editing web.config appropriately, and by wrapping exception-throwing code in try/catch blocks.

Once visibility is handled, you also need to validate client input. ASP.NET only performs simple checks and transforms the XML into CLR types. Once the transformation is complete, you may need to do extra validation. As a result of this validation, the code will set a custom faultmessage and send some details to the caller as to what was wrong.

 

At Your Service

Scott Seely is a member of the MSDN Architectural Samples team. Besides his work there, Scott is the author of SOAP: Cross Platform Web Service Development Using XML (Prentice Hall—PTR) and the lead author for Creating and Consuming Web Services in Visual Basic (Addison-Wesley).