简体   繁体   中英

WCF: FaultContract set but not causing FaultException<T> to be caught in REST service

I'm just starting with WCF and am trying to verify fault handling in a WCF rest service that supports both JSON and XML. My test service generates a fault but no matter what I try I can't get my client to pick up the details of the fault (and the behavior varies based on the request format and http status code):

My test service generates the fault as follows:

public Data GetResponse()
{
    throw new WebFaultException<ErrorDetails>(
        new ErrorDetails {ErrorMessage = "Server Config Value not set"},
        HttpStatusCode.OK
        );
}

This gets sent across the wire quite reasonably:

{"ErrorMessage":"Server Config Value not set"}

And:

<ErrorDetails xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    <ErrorMessage>Server Config Value not set</ErrorMessage>
</ErrorDetails>

My client is defined with a FaultContract :

[OperationContract]
[WebInvoke(
    UriTemplate="/response",
    Method="GET",
    RequestFormat = WebMessageFormat.Xml, // or .Json
    ResponseFormat = WebMessageFormat.Xml // or .Json
    )]
[FaultContract(typeof(ErrorDetails), Namespace="")]
Data GetResponse();

Here's the full error message for (format/status code):

XML/Conflict:

Requesting response CommunicationException: The remote server returned an unexpected response: (409) Conflict., System.Collections.ListDictionary Internal, System.ServiceModel.ProtocolException Press a key to exit...

And XML/OK:

Requesting response Exception: Unable to deserialize XML body with root name 'ErrorDetails' and root namespace '' (for operation 'GetResponse' an d contract ('IClient', '')) using DataContractSerializer. Ensure that the type corresponding to the XML is added to the know n types collection of the service., System.Collections.ListDictionaryInternal Press a key to exit...

And JSON/Conflict:

Requesting response CommunicationException: The remote server returned an unexpected response: (409) Conflict., System.Collections.ListDictionary Internal, System.ServiceModel.ProtocolException Press a key to exit...

And JSON/OK:

Requesting response Response: Request complete Press a key to exit...

The client code catches the exceptions in the proper order:

try
{
    Console.WriteLine("Requesting response");
    Console.WriteLine("Response: " + client.GetResponse().Message);
    Console.WriteLine("Request complete");
}
// sanity check, just in case...
catch (WebFaultException<ErrorDetails> ex)
{
    Console.WriteLine("WebFaultException<ErrorDetails>: " + ex.Detail.ErrorMessage + ", " + ex.Reason);
}
catch (FaultException<ErrorDetails> ex)
{
    Console.WriteLine("FaultException<ErrorDetails>: " + ex.Detail.ErrorMessage + ", " + ex.Reason);
}
catch (FaultException ex)
{
    Console.WriteLine("FaultException: " + ex.Message + ", " + ex.Reason);
}
catch (CommunicationException ex)
{
    Console.WriteLine("CommunicationException: " + ex.Message + ", " + ex.Data + ", " + ex.GetType().FullName);
}
catch (Exception ex)
{
    Console.WriteLine("Exception: " + ex.Message + ", " + ex.Data);
}

What do I have to do so that FaultException<ErrorDetails> will be thrown and I can get access to the ErrorDetails ?

Note: The gist should be fully compileable and runnable.

It's not possible to use Faults/FaultContract as an error handling mechanism in WCF REST clients (as of WCF version 4.0) service:

Per one thread on the WCF list:

In WCF Rest service there is not SOAP message, so you can't return a FaultException to the client. Actually the appropriate status code is returned to the requestor as an HTTP header, allowing the requestor to determine the result of the call.

And from a StackOverflow post :

Faults are part of SOAP protocol and are not available in REST scenarios

Options considered

FaultContract - The FaultContractAttribute works for SOAP envelopes, but not for WCF REST services. It's possible that if you send XML across that's formatted as a SOAP envelope that you could get it to work, but then you're not using reasonable custom error messages.

IErrorHandler - A quick read of the documentation indicates this is for the service and considering it was a newbie error: "Allows an implementer to control the fault message returned to the caller and optionally perform custom error processing such as logging."

Message Inspectors - The AfterReceiveRequest doesn't execute because the CommunicationException is thrown first.

Workaround

After quite a bit of digging I found one blogger who recommended creating a helper class to extract the information from response stream that's embedded in the exception objects. Here's his implementation, as is:

public static void HandleRestServiceError(Exception exception, Action<TServiceResult> serviceResultHandler, Action<TServiceFault> serviceFaultHandler = null, Action<Exception> exceptionHandler = null)
{
  var serviceResultOrServiceFaultHandled = false;

  if (exception == null) throw new ArgumentNullException("exception");
  if (serviceResultHandler == null) throw new ArgumentNullException("serviceResultHandler");

  // REST uses the HTTP procol status codes to communicate errors that happens on the service side.
  // This means if we have a teller service and you need to supply username and password to login
  // and you do not supply the password, a possible scenario is that you get a 400 - Bad request.
  // However it is still possible that the expected type is returned so it would have been possible 
  // to process the response - instead it will manifest as a ProtocolException on the client side.
  var protocolException = exception as ProtocolException;
  if (protocolException != null)
  {
    var webException = protocolException.InnerException as WebException;
    if (webException != null)
    {
      var responseStream = webException.Response.GetResponseStream();
      if (responseStream != null)
      {
        try
        {
          // Debugging code to be able to see the reponse in clear text
          //SeeResponseAsClearText(responseStream);

          // Try to deserialize the returned XML to the expected result type (TServiceResult)
          var response = (TServiceResult) GetSerializer(typeof(TServiceResult)).ReadObject(responseStream);
          serviceResultHandler(response);
          serviceResultOrServiceFaultHandled = true;
        }
        catch (SerializationException serializationException)
        {
          // This happens if we try to deserialize the responseStream to type TServiceResult
          // when an error occured on the service side. An service side error serialized object 
          // is not deserializable into a TServiceResult

          // Reset responseStream to beginning and deserialize to a TServiceError instead
          responseStream.Seek(0, SeekOrigin.Begin);

          var serviceFault = (TServiceFault) GetSerializer(typeof(TServiceFault)).ReadObject(responseStream);

          if (serviceFaultHandler != null && serviceFault != null)
          {
            serviceFaultHandler(serviceFault);
            serviceResultOrServiceFaultHandled = true;
          }
          else if (serviceFaultHandler == null && serviceFault != null)
          {
            throw new WcfServiceException<TServiceFault>() { ServiceFault = serviceFault };
          }
        }
      }
    }
  }

  // If we have not handled the serviceResult or the serviceFault then we have to pass it on to the exceptionHandler delegate
  if (!serviceResultOrServiceFaultHandled && exceptionHandler != null)
  {
    exceptionHandler(exception);
  }
  else if (!serviceResultOrServiceFaultHandled && exceptionHandler == null)
  {
    // Unable to handle and no exceptionHandler passed in throw exception to be handled at a higher level
    throw exception;
  }
}

I've used this:

 try
            {
              //wcf service call
            }
            catch (FaultException ex)
            {
               throw new Exception( (ex as WebFaultException<MyContractApplicationFault>).Detail.MyContractErrorMessage );                
            }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM