none
How to return a FaultException<> from a WCF client that calls a REST API

    Question

  • I am writing a WCF client to access a REST Query service published by a 3rd party.  If the request is valid, everything works.  When a request is invalid, the REST service returns an HTTP Result Code 4xx and an XML block with additional information:


    <ErrorResponse>
     <Error>
      <Type>Sender</Type>
      <Code>InvalidActivationKey</Code>
      <Message>The Activation Key is invalid or malformed</Message>
      <Detail/>
     </Error>
     <RequestID>42711328-49cc-45dd-924d-379ba72d9cd6</RequestID>
    </ErrorResponse>
    


    My service contract looks like this:


    [ServiceContract(Namespace = LicensingServices.LicenseServiceNamespace)]
    interface IAmazonLicenseService
    {
        [OperationContract]
        [WebGet(UriTemplate =
            "?Action=ActivateDesktopProduct" +
            "&ActivationKey={activationKey}" +
            "&ProductToken={productToken}" +
            "&Version={version}")]
        [FaultContract(typeof(AmazonErrorResponse), Namespace=LicensingServices.LicenseServiceNamespace)]
        ActivateDesktopProductResponse ActivateDesktopProduct(
            string activationKey,
            string productToken,
            string version);
    }
    


    I have defined a DataContract called AmazonErrorResponse that is modelled after the XML returned.

    I have created a custom WebChannelFactory to inject a custom WebHttpBehavior so that I can override the default error handling which is throwing a ProtocolException.  Here's some of that code:


    class AmazonWebChannelFactory<T> : WebChannelFactory<T>
        where T: class
    {
        public AmazonWebChannelFactory(Uri remoteAddress)
            : base(remoteAddress) { }
    
        public override T CreateChannel(EndpointAddress address, Uri via)
        {
            // We add our own WebHttpBehavior here so that it can customize the error handling.
            Endpoint.Behaviors.Add(new AmazonWebHttpBehavior());
    
            return base.CreateChannel(address, via);
        }
    }
    
    class AmazonWebHttpBehavior : WebHttpBehavior, IErrorHandler
    {
        protected override void AddClientErrorInspector(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
            clientRuntime.CallbackDispatchRuntime.ChannelDispatcher.ErrorHandlers.Add(this);
        }
    
        public bool HandleError(Exception error)
        {
            return false;
        }
    
        public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
        {
            Debug.WriteLine("ProvideFault");
        }
    
    }
    

    Unfortunately, my ProvideFault method is never called.  I suspect this is because I'm wiring it up wrong, but I just don't know where to go from here.  Any thoughts?
    • Edited by Doug Clutter Thursday, December 03, 2009 9:34 PM Formatting
    Thursday, December 03, 2009 9:15 PM

Answers

  • Hi Doug,

    In this situation we can directly use HttpWebRequest object and parse returned web response to implement this requirement. Please have a look at this code sample for your reference:
    http://msdn.microsoft.com/en-us/library/cc656724.aspx
    HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
    Console.WriteLine("Client: Recieve Response HTTP/{0} {1} {2}", resp.ProtocolVersion, (int)resp.StatusCode, resp.StatusDescription);


    Best regards,
    Riquel
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.
    Welcome to the All-In-One Code Framework! If you have any feedback, please tell us.
    • Marked as answer by Doug Clutter Wednesday, December 09, 2009 11:40 AM
    Wednesday, December 09, 2009 5:41 AM
    Moderator

All replies

  • Hi Doug,

    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. We can setting the status code on the response to give the caller more information in this situation. Please have a look at this article:
    http://www.robbagby.com/rest/effective-error-handling-with-wcf-rest/

    Best regards,
    Riquel
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.
    Welcome to the All-In-One Code Framework! If you have any feedback, please tell us.
    Monday, December 07, 2009 9:25 AM
    Moderator
  • Hi Riquel,

    Thank you for your reply.  I reviewed the link you gave and it focuses on creating a server.  That is not my situation.

    I am accessing a service from a third party and I have no control over the service.  I am trying to create a client that responds with a FaultException<> when the service returns an error.

    The default behavior of WCF is to throw a ProtocolException and discard all the information returned. 

    I have created a Console application that should illustrate what I'm trying to do.  I am new to WCF, so I am very open to other approaches.  Thank you again for your feedback.

    The sample code:

    using System;
    using System.ServiceModel;
    using System.ServiceModel.Description;
    using System.ServiceModel.Web;
    using System.Runtime.Serialization;
    using System.ServiceModel.Dispatcher;
    using System.ServiceModel.Channels;
    
    namespace WcfRestClientError
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.WriteLine("To see a sample response from the server, open this Uri in a browser:");
                Console.WriteLine(@"https://ls.amazonaws.com/?Action=ActivateDesktopProduct&ActivationKey=BadActivationKey&ProductToken={ProductToken}SampleProductTokenIsNotValidxAAXBwVGtuvFsiDcE5I2o0thZmL5C9Db8V2Hc8UBLWKuaLic2GFqutnd95gyf5JkiINIAS4JfYYAEOrITd9mlSbIj8SYXOByWA91i%2fpH3H%2b23CROPjPXNDUllx9hwIThYNpDSYwIot53j3lVKXo1tqB5TPaDCWOObnxBbY6UEttrq2ZWM5IbGFN3YK%2bcj%2fPhmkgjp1wRNjEtzT1kneyKz4MFajORy8YBsccIBbnFfdjGc1V5dY20WZ8PsFYhi%2bCXF3a6eWj8%2fv8shYbuYoxXONRg2mXJqjuxptngLbIVRiIembFmycidg%3d&Version=2008-04-28");
                Console.WriteLine();
    
                Console.WriteLine("Contacting License Server...");
                ActivateDesktopProductResponse response = Activate("BadActivationKey");
                if (response != null)
                {
                    // This part of the code works perfectly.
                    // I cannot provide an example because Activation keys expire in one hour.  
                    Console.WriteLine("Activation successful!");
                    Console.WriteLine("User Token: " + response.ActivateDesktopProductResult.UserToken);
                }
                Console.Write("Hit a key to exit...");
                Console.ReadKey();
            }
            public static ActivateDesktopProductResponse Activate(string activationKey)
            {
                try
                {
                    using (AmazonWebChannelFactory<IAmazonLicenseService> cf =
                        new AmazonWebChannelFactory<IAmazonLicenseService>(new Uri(LicensingServices.LicenseUri)))
                    {
                        // Call licensing server
                        IAmazonLicenseService service = cf.CreateChannel();
                        ActivateDesktopProductResponse response = service.ActivateDesktopProduct(
                            activationKey,
                            LicensingServices.ProductIdentificationToken,
                            LicensingServices.LicenseServiceVersion);
    
                        return response;
                    }
                }
                catch (FaultException<AmazonErrorResponse> ex)
                {
                    Console.WriteLine("SHOULD HIT THIS EXCEPTION --- SO WE CAN GET ERROR INFO FROM SERVER");
                    Console.WriteLine("Error Code: " + ex.Detail.Error.Code);
                    Console.WriteLine("Error Message: " + ex.Detail.Error.Message);
                    return null;
                }
                catch (ProtocolException ex)
                {
                    Console.WriteLine("WE DO NOT WANT TO CATCH THIS EXCEPTION BECAUSE WE LOSE THE ERROR INFO RETURNED BY THE SERVER");
                    Console.WriteLine("Protocol error: " + ex.Message);
                    return null;
                }
                catch (TimeoutException)
                {
                    Console.WriteLine("Timed out");
                    return null;
                }
                catch (CommunicationException ex)
                {
                    Console.WriteLine("Communications error: " + ex.Message);
                    return null;
                }
            }
        }
        static class LicensingServices
        {
            internal const string LicenseServiceNamespace = "http://ls.amazonaws.com/doc/2008-04-28/";
            internal const string LicenseUri = "https://ls.amazonaws.com";
            internal const string LicenseServiceVersion = "2008-04-28";
            internal const string ProductIdentificationToken = "{ProductToken}SampleProductTokenIsNotValidx5C9Db8V2Hc8UBLWKuaLic2GFqutnd95gyf5JkiINIAS4JfYYAEOrITd9mlSbIj8SYXOByWA91i/pH3H+23CROPjPXNDUllx9hwIThYNpDSYwIot53j3lVKXo1tqB5TPaDCWOObnxBbY6UEttrq2ZWM5IbGFN3YK+cj/Phmkgjp1wRNjEtzT1kneyKz4MFajORy8YBsccIBbnFfdjGc1V5dY20WZ8PsFYhi+CXF3a6eWj8/v8shYbuYoxXONRg2mXJqjuxptngLbIVRiIembFmycidg=";
        }
        [ServiceContract(Namespace = LicensingServices.LicenseServiceNamespace)]
        interface IAmazonLicenseService
        {
            [OperationContract]
            [WebGet(UriTemplate =
                "?Action=ActivateDesktopProduct" +
                "&ActivationKey={activationKey}" +
                "&ProductToken={productToken}" +
                "&Version={version}")]
            [FaultContract(typeof(AmazonErrorResponse), Namespace = LicensingServices.LicenseServiceNamespace)]
            ActivateDesktopProductResponse ActivateDesktopProduct(
                string activationKey,
                string productToken,
                string version);
        }
        class AmazonWebChannelFactory<T> : WebChannelFactory<T>
            where T : class
        {
            public AmazonWebChannelFactory(Uri remoteAddress)
                : base(remoteAddress) { }
    
            protected override void OnOpening()
            {
                Console.WriteLine("AmazonWebChannelFactory.OnOpening");
                // We add our own WebHttpBehavior here so that it can customize the error handling.
                Endpoint.Behaviors.Add(new AmazonWebHttpBehavior());
    
                base.OnOpening();
            }
        }
        class AmazonWebHttpBehavior : WebHttpBehavior, IErrorHandler
        {
            protected override void AddClientErrorInspector(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
            {
                Console.WriteLine("AmazonWebHttpBehavior.AddClientErrorInspector");
                clientRuntime.CallbackDispatchRuntime.ChannelDispatcher.ErrorHandlers.Clear();
                clientRuntime.CallbackDispatchRuntime.ChannelDispatcher.ErrorHandlers.Add(this);
            }
    
            public bool HandleError(Exception error)
            {
                Console.WriteLine("AmazonWebHttpBehavior.HandleError");
                return false;
            }
    
            public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
            {
                Console.WriteLine("AmazonWebHttpBehavior.ProvideFault - EXPECTED TO HIT THIS LINE, BUT WE NEVER DO");
    
                // Create the FaultException here from information passed in
                // How do we extract the information needed to build the AmazonError and AmazonErrorResponse?
                AmazonError errorInfo = new AmazonError() { Code = "???", Detail = "???", Message = "???", Type = "???"};
                AmazonErrorResponse errorResponse = new AmazonErrorResponse() { Error = errorInfo, RequestID = "???"};
                throw new FaultException<AmazonErrorResponse>(errorResponse);
            }
        }
        [DataContract(Namespace = LicensingServices.LicenseServiceNamespace)]
        internal class ResponseMetadata
        {
            [DataMember]
            public string RequestId { get; set; }
        }
        [DataContract(Namespace = LicensingServices.LicenseServiceNamespace)]
        internal class ActivateDesktopProductResult
        {
            [DataMember]
            public string UserToken { get; set; }
    
            [DataMember]
            public string AWSAccessKeyId { get; set; }
    
            [DataMember]
            public string SecretAccessKey { get; set; }
        }
        [DataContract(Namespace = LicensingServices.LicenseServiceNamespace)]
        internal class ActivateDesktopProductResponse
        {
            [DataMember]
            public ActivateDesktopProductResult ActivateDesktopProductResult { get; set; }
    
            [DataMember]
            public ResponseMetadata ResponseMetadata { get; set; }
        }
        [DataContract(Namespace = LicensingServices.LicenseServiceNamespace)]
        internal class AmazonErrorResponse
        {
            [DataMember]
            public AmazonError Error { get; set; }
    
            [DataMember]
            public string RequestID { get; set; }
        }
    
        [DataContract(Namespace = LicensingServices.LicenseServiceNamespace)]
        public class AmazonError
        {
            [DataMember]
            public string Type { get; set; }
    
            [DataMember]
            public string Code { get; set; }
    
            [DataMember]
            public string Message { get; set; }
    
            [DataMember]
            public string Detail { get; set; }
        }
    }
    
    Monday, December 07, 2009 1:56 PM
  • Hi Doug,

    We implement the IErrorHandler interface to control the fault message returned to the caller and optionally perform custom error processing such as logging. This is the service behavior, not the client behavior. Please have a look at this article for your reference:
    http://blog.wadolabs.com/page/2/

    Actually you need to check the web Response information to obtain all information in the REST client to implement your requirement.

    Best regards,
    Riquel
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.
    Welcome to the All-In-One Code Framework! If you have any feedback, please tell us.
    Tuesday, December 08, 2009 8:51 AM
    Moderator
  • Hi Riquel,

    Actually you need to check the web Response information to obtain all information in the REST client to implement your requirement.

    Yes.  After tinkering with this for a few days, I've come to the same conclusion.  Do you know where I could find some good sample code that would let me see the response information BEFORE the ProtocolException is tossed out?

    Best regards,
    Doug
    Tuesday, December 08, 2009 11:14 AM
  • Hi Doug,

    In this situation we can directly use HttpWebRequest object and parse returned web response to implement this requirement. Please have a look at this code sample for your reference:
    http://msdn.microsoft.com/en-us/library/cc656724.aspx
    HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
    Console.WriteLine("Client: Recieve Response HTTP/{0} {1} {2}", resp.ProtocolVersion, (int)resp.StatusCode, resp.StatusDescription);


    Best regards,
    Riquel
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.
    Welcome to the All-In-One Code Framework! If you have any feedback, please tell us.
    • Marked as answer by Doug Clutter Wednesday, December 09, 2009 11:40 AM
    Wednesday, December 09, 2009 5:41 AM
    Moderator
  • Hi Doug,

    Yes, as Riquel mentioned, the FaultContract is only useful for webservice that will return standard SOAP format message. And Faulcontract will only map to xml element contained in the  soap:Fault element in SOAP message. For your scenario, the response message is a custom XML format which will not work with WCF Faulcontract system.

    To the new question you mentioned on " let me see the response information BEFORE the ProtocolException is tossed out?", I think you can have a look at MessageInspector:

    MessageInspector is one of the extension component for WCF, it can be applied on both service side and client side for us to view or modify the underlying message:

    #Message Inspectors
    http://msdn.microsoft.com/en-us/library/aa717047.aspx

    http://blogs.msdn.com/stcheng/archive/2009/03/17/wcf-use-messageinspector-to-validate-soap-xml-message.aspx

    #How to: Inspect or Modify Messages on the Client
    http://msdn.microsoft.com/en-us/library/ms733786.aspx

     


    Please remember to mark the replies as answers if they help and unmark them if they provide no help.
    Wednesday, December 09, 2009 10:27 AM
    Moderator
  • Riquel,

    Thank you for suggesting another approach.  It is very disheartening to find that WCF cannot handle such a simple case, but I suppose nothing can do everything.

    Best regards,
    Doug
    Wednesday, December 09, 2009 11:40 AM
  • Hi Steven,

    I did try using a Message Inspector, but the ProtocolException is thrown before the "After" method is called.

    Thanks for the suggestion though!

    Best regards,
    Doug
    Wednesday, December 09, 2009 11:41 AM
  • Hi Doug,

    In this situation if the service vendor can consider WCF REST Starter Kit to use the WebProtocolException, it can give some useful information to client application.


    Best regards,
    Riquel
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.
    Welcome to the All-In-One Code Framework! If you have any feedback, please tell us.
    Friday, December 11, 2009 3:17 AM
    Moderator
  • Hi Riquel,

    Given the "Preview" status of the REST Starter Kit, I'm just not comfortable relying on it.  Thanks for suggesting it.

    Best regards,
    Doug
    Friday, December 11, 2009 11:44 AM
  • Hi Doug,

    I know this is a long dead posting, but I think I accidentally experienced the same situation as you. I spent some time trying to figure out what was going on and ended up with a possible solution to the problem. I have described my solution in a blog post that can be found at http://kenneththorman.blogspot.com/2011/02/wcf-rest-exception-handling.html the Visual Studio 2010 solution is also available there.

    Regards

    Kenneth Thorman

    Thursday, November 24, 2011 8:23 PM