none
Returing custom JSON exceptions from a REST WCF service that calls another WCF service RRS feed

  • Question

  • Hi,

    I have a REST WCF service (Service1) exposed with WebHttp and called from a web page using jQuery ajax (WebClient). Service1 has a second endpoint exposed with NetTcp and called from another service (Service2).  Service2 is also a REST WCF service that is exposed with WebHttp and called from WebClient.

    When called, Service1 throws an exception: throw new Exception("Error in Service 1");.  Custom IErrorHandler is used to turn it into a FaultException and serialize as JSON.
    When WebClient  calls Service1 directly, it gets the JSON error in jqxhr.responseText. But when it calls Service2 that calls Service1 JSON error is not returned and instead client gets a generic HTML response  containing “The server encountered an error processing the request. See server logs for more details” message in the jqxhr.responseText.
    On the server side code handling is identical and generates same response messages, but WCF seems to be replacing it with it's own in the second case:

    Service1 and Service2 code:

    [DataContract(Namespace = "WcfFaultRouting")] public class Fault { public const string Action = "Error"; [DataMember] public string Message { get; set; } } [ServiceContract] public interface IService { [OperationContract, WebGet(UriTemplate = "/Test")] [FaultContract(typeof(Fault), Action = Fault.Action)] string Test(); } [FaultProvier] public class Service1 : IService { public string Test() { //return "Hello from Service 1"; throw new Exception("Error in Service 1"); } } [FaultProvier] public class Service2 : IService { public string Test() { using (var client = new Service1Client()) { return client.Test(); } } } internal class Service1Client : ClientBase<IService>, IService { public string Test() { return base.Channel.Test(); } }

    FaultProvider code:

    public class FaultProvier : Attribute, IServiceBehavior
    {
        #region ErrorHandler
        internal class ErrorHandler : IErrorHandler
        {
            internal ErrorHandler()
            {
            }
            public bool HandleError(System.Exception error)
            {
                return true;
            }
            public void ProvideFault(System.Exception error, System.ServiceModel.Channels.MessageVersion version, ref System.ServiceModel.Channels.Message message)
            {
                if (!(error is FaultException/*<WcfFaultRouting.Fault>*/))
                {
                    var fault = new WcfFaultRouting.Fault();
                    if (OperationContext.Current != null)
                    {
                        if (String.IsNullOrWhiteSpace(OperationContext.Current.IncomingMessageHeaders.Action)) //not a SOAP call
                        {
                            fault.Message = error.Message;
                            toJson(fault, ref message);
                        }
                        else if (WebOperationContext.Current != null)
                        {
                            var fe = new FaultException<WcfFaultRouting.Fault>(fault, error.Message);
                            var mf = fe.CreateMessageFault();
                            message = System.ServiceModel.Channels.Message.CreateMessage(version, mf, WcfFaultRouting.Fault.Action);
                            WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.OK; //http://msdn.microsoft.com/en-us/library/dd470096(VS.95).aspx
                        }
                    }
                }
                else if (OperationContext.Current != null)
                {
                    if (String.IsNullOrWhiteSpace(OperationContext.Current.IncomingMessageHeaders.Action)) //not a SOAP call
                    {
                        var fault = new WcfFaultRouting.Fault();
                        fault.Message = error.Message;
                        toJson(fault, ref message);
                    }
                    else if (WebOperationContext.Current != null) //SL client
                        WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.OK; //http://msdn.microsoft.com/en-us/library/dd470096(VS.95).aspx
                }
            }
            void toJson(WcfFaultRouting.Fault fault, ref System.ServiceModel.Channels.Message message)
            {
                //When a WebHttp call is routed via another service - JSON payload (Fault object) is not returned
                //Instead the client gets the following responseText: Request Error 
                //The server encountered an error processing the request. See server logs for more details
                //if (message != null)
                //{
                    //Tried the following to no avail:
                    //message = System.ServiceModel.Channels.Message.CreateMessage(MessageVersion.None, WcfFaultRouting.Fault.Action, fault, new DataContractJsonSerializer(typeof(WcfFaultRouting.Fault)));
                    //HttpResponseMessageProperty httpResponse = new HttpResponseMessageProperty();
                    //httpResponse.StatusCode = System.Net.HttpStatusCode.InternalServerError;
                    //httpResponse.StatusDescription = "Custom Fault";
                    //httpResponse.Headers[System.Net.HttpResponseHeader.ContentType] = "application/json";
                    //message.Properties.Add(HttpResponseMessageProperty.Name, httpResponse);
                    //message.Properties[WebBodyFormatMessageProperty.Name] = new WebBodyFormatMessageProperty(WebContentFormat.Json);
                    //WebOperationContext.Current.OutgoingResponse.Format = WebMessageFormat.Json;
                    //WebOperationContext.Current.OutgoingResponse.ContentType = "application/json";
                //}
                //else
                //{
                    message = WebOperationContext.Current.CreateJsonResponse<WcfFaultRouting.Fault>((WcfFaultRouting.Fault)fault, new DataContractJsonSerializer(typeof(WcfFaultRouting.Fault)));
                //}
                var response = WebOperationContext.Current.OutgoingResponse;
                response.StatusCode = System.Net.HttpStatusCode.InternalServerError;
                response.StatusDescription = "Custom Fault";
            }
        }
        #endregion
        public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
        {
        }
        public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
        {
            foreach (ChannelDispatcher cDispatcher in serviceHostBase.ChannelDispatchers)
            {
                cDispatcher.ErrorHandlers.Add(new ErrorHandler());
            }
        }
        public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
        {
        }
    }

    App.config:

    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
      <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
      </startup>
      <system.serviceModel>
        <client>
          <endpoint address="net.tcp://localhost:9800/Service1" binding="netTcpBinding" bindingConfiguration="NetTcpBinding" contract="WcfFaultRouting.Client.IService" name="WcfFaultRouting.Client.Service1" />
        </client>
        <services>
          <service name="WcfFaultRouting.Service1">
            <endpoint address="http://localhost:8080/Service1" binding="webHttpBinding" behaviorConfiguration="WebHttpBehavior" contract="WcfFaultRouting.IService" />
            <endpoint address="net.tcp://localhost:9800/Service1" binding="netTcpBinding" bindingConfiguration="NetTcpBinding" contract="WcfFaultRouting.IService" />
          </service>
          <service name="WcfFaultRouting.Service2">
            <endpoint address="http://localhost:8080/Service2" binding="webHttpBinding" behaviorConfiguration="WebHttpBehavior" contract="WcfFaultRouting.IService" />
          </service>
        </services>
        <bindings>
          <netTcpBinding>
            <binding name="NetTcpBinding" maxBufferPoolSize="2147483647" maxReceivedMessageSize="2147483647" maxBufferSize="2147483647">
              <security mode="None" />
              <readerQuotas maxArrayLength="2147483647" maxStringContentLength="2147483647" />
            </binding>
          </netTcpBinding>
          <webHttpBinding>
            <binding name="WebHttpBinding" />
          </webHttpBinding>
        </bindings>
        <behaviors>
          <endpointBehaviors>
            <behavior name="WebHttpBehavior">
              <webHttp defaultBodyStyle="WrappedRequest" defaultOutgoingResponseFormat="Json" automaticFormatSelectionEnabled="false" />
            </behavior>
          </endpointBehaviors>
        </behaviors>
        <diagnostics wmiProviderEnabled="true">
          <messageLogging logEntireMessage="true" 
                          logMalformedMessages="true" 
                          logMessagesAtServiceLevel="false" 
                          logMessagesAtTransportLevel="true"  
                          maxMessagesToLog="5000" />
        </diagnostics>
      </system.serviceModel>
      <system.diagnostics>
        <sources>
          <source name="System.ServiceModel" switchValue="Warning" propagateActivity="true" >
            <listeners>
              <add name="xml"/>
            </listeners>
          </source>
          <source name="System.ServiceModel.MessageLogging">
            <listeners>
              <add name="xml" />
            </listeners>
          </source>
        </sources>
        <sharedListeners>
          <add name="xml" type="System.Diagnostics.XmlWriterTraceListener" initializeData="service.svclog" />
        </sharedListeners>
        <trace autoflush="true" />
      </system.diagnostics>
    </configuration>

    Web Client sript:

    <script src="/jquery-1.8.3.js" type="text/javascript"></script>
    <script type="text/javascript">
        jQuery.support.cors = true; //Required for IE</text>
        jQuery.ajaxSetup({
            error: function (jqxhr, settings, error) {
                debugger;
                //When Service1 is called directly jqxhr.responseText will be "{\"Message\":\"Error in Service 1\"}"
                //When it is called via Service2 it will return "<html>...The server encountered an error processing the request. See server logs for more details...</html>"
                alert("AJAX error " + jqxhr.responseText);
            }
        });
        $(document).ready(function () {
            ajaxSettings = {
                dataType: 'json',
                contentType: 'application/json',
                url: "http://localhost:8080/Service1/Test",
            };
            ajaxSettings.callback = function (result) {
            }
            jQuery.ajax(ajaxSettings);
            ajaxSettings = {
                dataType: 'json',
                contentType: 'application/json',
                url: "http://localhost:8080/Service2/Test",
            };
            ajaxSettings.callback = function (result) {
            }
            jQuery.ajax(ajaxSettings);
        });
    </script>

    Ay help iss appreciated, I can also upload a solution zip.

    Thanks.

    Thursday, January 17, 2013 6:47 PM

All replies

  • Wcf trace log:

    WebClient -> Service1

    Request

    <MessageLogTraceRecord>
    <HttpRequest xmlns="http://schemas.microsoft.com/2004/06/ServiceModel/Management/MessageTrace">
    <Method>GET</Method>
    <QueryString></QueryString>
    <WebHeaders>
    <Connection>Keep-Alive</Connection>
    <Content-Type>application/json</Content-Type>
    <Accept>application/json, text/javascript, */*; q=0.01</Accept>
    <Accept-Encoding>gzip, deflate</Accept-Encoding>
    <Accept-Language>en-ca</Accept-Language>
    <Host>localhost:8080</Host>
    <Referer>http://localhost:57241/TestPage.html</Referer>
    <User-Agent>Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)</User-Agent>
    </WebHeaders>
    </HttpRequest>
    </MessageLogTraceRecord> 

    Service1 response

    <MessageLogTraceRecord>
    <Addressing xmlns="http://schemas.microsoft.com/2004/06/ServiceModel/Management/MessageTrace">
    <Action></Action>
    </Addressing>
    <HttpResponse xmlns="http://schemas.microsoft.com/2004/06/ServiceModel/Management/MessageTrace">
    <StatusCode>InternalServerError</StatusCode>
    <StatusDescription>Custom Fault</StatusDescription>
    <WebHeaders>
    <Content-Type>application/json; charset=utf-8</Content-Type>
    </WebHeaders>
    </HttpResponse>
    <root type="object" xmlns="">
    <Message type="string">Error in Service 1</Message>
    </root>
    </MessageLogTraceRecord>
    Thursday, January 17, 2013 6:53 PM
  • Also found this post:

    How to get FaultException back to client from a Service Router

    It is not exactly the same as in the above example the channel was faulting (could be because exception may not have been a FaultException to begin with), and solution was to throw a new FaultException (as opposed to re-throwing) in the intermediary e.q Router, or Service2 in this case.

    I gave it a try anyway and similar approach seems to be working for me too:

    internal class Service1Client : ClientBase<IService>, IService
    {
        public string Test()
        {
            try
            {
                return base.Channel.Test();
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }
    }

    Either way ProvideFault would execute the same code:

    var fault = new WcfFaultRouting.Fault();
    fault.Message = error.Message;
    toJson(fault, ref message);

    The difference however is that when ProvideFault is called for Service 2 in case of re-throwing - error argument is already a FaultException and the message is already instantiated, but it is null when new Exception is thrown from the Service1Client.

    Can anybody  offer an explanation if it is actually needed to throw a brand new exception in WCF mediation scenario or re-throwing is expected and if there are any alternative solutions. Thanks.

    • Edited by govis Thursday, January 17, 2013 11:13 PM
    Thursday, January 17, 2013 10:49 PM
  • Hi,

    Thanks for your post.

    I am trying to involve someone familiar with this topic to further look at this issue. There might be some time delay. Appreciate your patience.

    Best Regards.


    Haixia
    MSDN Community Support | Feedback to us
    Develop and promote your apps in Windows Store
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    Monday, January 21, 2013 9:38 AM
    Moderator
  • Hi Haixia,

    I have simlified the case to a single service that thows an exception and a custom error handler provider that generates a Json reply message.

    Please have a look here:

    Returing custom JSON message from exception in REST WCF service

    Thanks

    Wednesday, February 20, 2013 5:26 PM