Ground rules:
In WCF it is not recommended to throw .NET exceptions since they most likely are not known and recognized in non .NET clients. so instead we are using FaultException<TDetail> and WebFaultException. The below quote is taken from the MSDN WebFaultException documentation.
When using a WCF REST endpoint (WebHttpBinding and WebHttpBehavior or WebScriptEnablingBehavior) the HTTP status code on the response is set accordingly. However, WebFaultException can be used with non-REST endpoints and behaves like a regular FaultException.Thoughts and implementation
When using a WCF REST endpoint, the response format of the serialized fault is determined in the same way as a non-fault response. For more information about WCF REST formatting, see WCF REST Formatting.
My intial take on how to handle service side generated faults/errors was to use the IClientMessageInspector & Message Inspectors for fault handling. However nothing seemed to work. I got a ProtocolException that was thrown from deep in the WCF REST implementation. The exception was thrown by the underlying WCF REST implementation before the IClientMessageInspector.AfterReceiveReply method was invoked. After spending some time in Reflector with a fellow developer it indeed seemed like there are no extensibility points you can hook into. As soon as a WCF REST service throws a WebFaultException with a fault indicating http response code it will trigger a ProtocolException on the WCF REST client side.
So it seems like you have to solve this with wrapping the proxy calls in a try catch block and then extract the InnerException from the ProtocolException and convert this from a Stream ... etc, etc, etc (i.e. repetitive plumbing code) . Since this plumbing code will be the same for most if not all clients, I created a helper class that should work for any service.
The code that is used here is building on code that has been explained in the following blog postings:
The full solution can be downloaded here WcfRestClientExceptionHandling.7z.
Try running the client form with the default app.config and then try changing
<customHttpHeaders> <headers> <add key="MyCustomHttpHeader" value="some_value"></add> <add key="MyCustomHttpHeader2" value="yet_another_value"></add> </headers> </customHttpHeaders>
to the following
<customHttpHeaders> <headers> </headers> </customHttpHeaders>
First let us define our service.
using System.Net; using System.ServiceModel; using System.ServiceModel.Activation; using System.ServiceModel.Web; using Common; namespace WcfRestService1 { // Start the service and browse to http://<machine_name>:<port>/Service1/help to view the service's generated help page // NOTE: By default, a new instance of the service is created for each call; change the InstanceContextMode to Single if you want // a single instance of the service to process all calls. [ServiceContract] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] public class Service1 { [WebGet(UriTemplate = "{id}")] public SampleItem[] GetByProductId(string id) { if (WebOperationContext.Current.IncomingRequest.Headers["MyCustomHttpHeader"] == null) throw new WebFaultException<SampleError>(new SampleError(){ErrorCode = "1223456789", Message = "The custom httpheader 'MyCustomHttpHeader' has not been set."}, HttpStatusCode.BadRequest); return new[] { new SampleItem() { Id = 1, StringValue = "Product1" } }; } } }Looking through the code we see that there are 2 possible objects returned (ignoring all the potential unhandled exceptions). The 2 possible objects are SampleItem, SampleError.
using System.Runtime.Serialization; namespace Common { [DataContract] public class SampleError { [DataMember] public string Message { get; set; } [DataMember] public string ErrorCode { get; set; } } }
using System.Runtime.Serialization; namespace Common { [DataContract] public class SampleItem { [DataMember] public int Id { get; set; } [DataMember] public string StringValue { get; set; } } }I will not get into the additional classes used to add custom http headers for the requests as they are explained in the previous indicated postings. Suffice to say that you can add and modify the headers included in the call to the WCF service by editing the customHeaders/headers element in the app.config file.
<behaviors> <endpointBehaviors> <behavior name="CustomHeaderBehavior"> <customHttpHeaders> <headers> <add key="MyCustomHttpHeader" value="some_value"></add> <add key="MyCustomHttpHeader2" value="yet_another_value"></add> </headers> </customHttpHeaders> <webHttp/> </behavior> </endpointBehaviors> </behaviors>Very briefly looking at the interface used to describe the service on the client side
using System.ServiceModel; using System.ServiceModel.Web; using Common; namespace WcfRestClient { [ServiceContract] public interface IService1Wrapper { [OperationContract] [WebGet( BodyStyle = WebMessageBodyStyle.Bare, ResponseFormat = WebMessageFormat.Xml, UriTemplate = "/Service1/GetByProductId?id={id}")] SampleItem[] GetProductById(string id); } }Now we will look at the client code. There are a few different outcomes of a service call.
Handled by proposed helper class:
- Everything is OK and the call returns the expected type deserialized on our/client end
- Everything is not OK and the service throws a FaultException and the http status code is one of the fault status codes. This will result in the current WCF REST implementation to raise a ProtocolException. The proposed code will try to handle that.
- Any other Exceptions thrown when trying to handle the result or fault from the service can be caught and handled or rethrown.
Not Handled by proposed helper class:
- Inconsistent use of http status codes and raising FaultExceptions. (Everything is not OK and the service throws a FaultException which needs to be handled on our client end. If this is combined with a http status code of 200/OK then we expect a TServiceResult but we are getting a TServiceFault. This will throw a SerializationException in the HandleRestServiceError() mehotd. The solution proposed here will not be able to handle that case. However one could argue that that is a problem at the service level.)
using System; using System.ServiceModel; using System.Windows.Forms; using Common; using WcfRestClient.WcfErrors; namespace WcfRestClient { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { var factory = new ChannelFactory<IService1Wrapper>("Service1WrapperREST"); var proxy = factory.CreateChannel(); try { // This call can return one of the following object as a serialized response // If successful -> SampleItem[] // If error -> SampleError // We can only handle SampleItem[]. If a combination of // a FaultException thrown + http status code OK then this code will // throw a SerializationException var serviceResult = proxy.GetProductById("1"); // In this case the serviceResult is returning the expected type HandleServiceResult(serviceResult); } catch (Exception exceptionThrownByRestWcfCall) { // There are a few different ways you can write the code below // Lambda expression with anonymous method // serviceResult => // { // MessageBox.Show("ServiceResult:" + serviceResult[0].StringValue); // }, // // Lambda expression with named method // serviceResult => HandleServiceResult(serviceResult) // // .NET 4.0 MethodGroup shorthand // HandleServiceResult // // MethodGroup shorthand is the shortest / clearest I think but I am // opting for the Lambda expression to keep the coding style the same. WcfRestExceptionHelper<SampleItem[], SampleError>.HandleRestServiceError( exceptionThrownByRestWcfCall, serviceResult => { HandleServiceResult(serviceResult); }, serviceFault => { MessageBox.Show(serviceFault.Message, "ServiceFaultHandler"); }, exception => { MessageBox.Show(exception.ToString(), "ExceptionHandler"); } ); } finally { ((IDisposable)proxy).Dispose(); } } private static void HandleServiceResult(SampleItem[] serviceResult) { MessageBox.Show(serviceResult[0].StringValue, "ServiceResult:"); } } }The interesting part is the following code
try { var serviceResult = proxy.GetProductById("1"); HandleServiceResult(serviceResult); } catch (Exception exceptionThrownByRestWcfCall) { WcfRestExceptionHelper<SampleItem[], SampleError>.HandleRestServiceError( exceptionThrownByRestWcfCall, serviceResult => { HandleServiceResult(serviceResult); }, serviceFault => { MessageBox.Show(serviceFault.Message, "ServiceFaultHandler"); }, exception => { MessageBox.Show(exception.ToString(), "ExceptionHandler"); } ); }That is a lot of code for a handling the responses from a simple service call, rewriting the code a bit (removing bracketed approach in the lambdas,
try { var serviceResult = proxy.GetProductById("1"); HandleServiceResult(serviceResult); } catch (Exception exceptionThrownByRestWcfCall) { WcfRestExceptionHelper<SampleItem[], SampleError>.HandleRestServiceError(exceptionThrownByRestWcfCall, HandleServiceResult, serviceFault => MessageBox.Show(serviceFault.Message, "ServiceFaultHandler")); }This allows us to put the actual handling code here where the service is called, without most of the plumbing code. The implementation of HandleRestServiceError() can be seen below.
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; } }After spending some time on this posting and coding it I found this forum post How to return a FaultException from a WCF client that calls a REST API. It does seem to come to a similar conclusion.
9 comments:
I have tried all possible way to solve this issue from last 10 days. Till issue exist.
I am having the below setup.
WCF Client <===TCP Binding==> WCF Routing Service <==== basicHTTPBinding====> WCF Service
My requirement is need to add the SessionID in the Cookie in the WCF Routing service.
I am using the IClientMessageInspector in Routing service as below and adding the HttpRequestMessageProperty properly. Before returning i check that cookie is added.
However, at the WCF server side i seen that Message.Properties is not received ( blank).
Not sure what is problem.
Pupublic object BeforeSendRequest(ref System.ServiceModel.Channels.Message request,
System.ServiceModel.IClientChannel channel)
{
HttpRequestMessageProperty httpRequestMessage;
object httpRequestMessageObject;
if (request.Properties.TryGetValue(HttpRequestMessageProperty.Name, out httpRequestMessageObject))
{
httpRequestMessage = httpRequestMessageObject as HttpRequestMessageProperty;
if (string.IsNullOrEmpty(httpRequestMessage.Headers[USER_COOKIEE]))
{
httpRequestMessage.Headers[USER_COOKIEE] = this.m_Session;
}
}
else
{
httpRequestMessage = new HttpRequestMessageProperty();
httpRequestMessage.Headers.Add(USER_COOKIEE, this.m_Session);
request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessage);
}
return null;
}
Also , i tried using the OperationContextScope as below , till unable to send the Custom HTTP headers at WCF service.
I checked headers in the Routing service all added headers exist, but when i see at WCF service not there. Please help me to resolve this issue.
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
get
HttpRequestMessageProperty httpRequestProperty = new HttpRequestMessageProperty();
httpRequestProperty.Headers.Add(HttpRequestHeader.Cookie, "CookieTestValue");
using (OperationContextScope scope = new OperationContextScope(channel)) //Create scope using the channel.
{
OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;
//OperationContext.Current.OutgoingMessageProperties.Add(HttpRequestMessageProperty.Name, httpRequestProperty);
//Also i added transport property as below.
OperationContext.Current.OutgoingMessageProperties.Add("TestHeader1"," TestHeaderVal 1213");
}
return null
}
Please let me know what is solution for this issue. Waiting for immediate reply.
-vivek
Very interesting. I observed the same issue. However, I found a way to do what I think you originally tried. I'm guessing your code was returning a status code of 400, which results in a ProtocolException thrown before message inspectors get called. If the exception thrown on the service side is a WebFaultException with a status code of 500, a message inspector (IClientMessageInspector)can pick it up and read the fault contract. I inserted my inspector first in the list of client message inspectors. Here's my inspector method for both Json and Xml (the ServiceFormat is my own custom property provided through the constructor of the message inspector):
public void AfterReceiveReply(ref Message reply, object correlationState, ServiceFormat serviceFormat)
{
System.Diagnostics.Debug.WriteLine(reply.Headers.ToString());
var props = (HttpResponseMessageProperty)reply.Properties[HttpResponseMessageProperty.Name];
int statusCode = (int)props.StatusCode;
if (statusCode >= 400)
{
Object faultError = null;
MessageBuffer messageBuffer = null;
try
{
// I create a copy
messageBuffer = reply.CreateBufferedCopy(int.MaxValue);
var reader = messageBuffer.CreateMessage().GetReaderAtBodyContents();
if (serviceFormat == ServiceFormat.Json)
{
var jsonSerializer = new DataContractJsonSerializer(typeof(FaultContract));
faultError = jsonSerializer.ReadObject(reader);
}
else if (serviceFormat == ServiceFormat.Xml)
{
var xmlSerializer = new DataContractSerializer(typeof(FaultContract));
faultError = xmlSerializer.ReadObject(reader);
}
}
catch (Exception)
{
// ??
faultError = null;
}
if (faultError != null && faultError is FaultContract)
{
var exception = new WebFaultException((FaultContract) faultError, props.StatusCode);
throw exception;
}
// If we didn't use the message, put it back
if (messageBuffer != null)
reply = messageBuffer.CreateMessage();
}
}
Using this mechanism, the client can catch the WebFaultException and use FaultContract in the Detail property.
Thanks, this solution worked like a charm for me. Well written explanation and clean simple solution.
Just what I was looking for. Thanks a whole lot!
Hi Kenneth,
It seems that this method does not work for all HTTP status codes. For example, if you return an HTTP status code of 500 (Internal Server Error) instead of a ProtocolException with an inner WebException you get a CommunicationException with no inner exception. Thus, there is no way to retrieve the information in web fault detail. On the other hand, it works like a charm for HTTP status code of 400 (Bad Request). I have not yet tested how it behaves for other HTTP status codes. Do you know why it behaves this way?
I am not doing WCF REST, but I just finished writing a reusable exception handling piece of code and I used a technique that you may also try using on this issue.
My issue was that I was getting a ProtocolException on a response from a WCF service call I was making to SAP (SAP-PI actually). I have lower level tracing code in my infrastructure that I can turn on and I can see the actually messages that flow back and forth at the SOAP level. (I LOVE how you can get down to that level with WCF!).
Well what I noticed down at that level is that there was good diagnostic information being sent back in the SOAP fault from SAP (a Java stack trace, etc.),but that information was no where to be found in the ProtocolException that was actually being thrown by WCF back to application code.
I poked all around and could not find anything, so what I did was this:
I added an event handler to catch the AppDomain's FirstChanceExcpetion event.
Immediately within that event handler, I check to see if it is a WebException, and if it is not, I return. In your case it might be some other exception you want to catch.
I created a static collection and a static method that I call when a WebException comes in. This method is thread safe, as you'll be getting hits from any active thread run by WCF. I capture information that I want from that WebException, and then added it to this static list and returned.
Now since WCF is multi-threaded, part of the info I saved was the Thread on which the WebException occurred.
Next, in the (reusable) exception handler for the ProtocolException, I have it call another static method and retrieve any captured data from the lower level WebException, FOR the current Thread. I can then report the low level SOAP information to my diagnostic log file which is a big help in diagnosing the problem.
One other trick in this is that the WebException has a "Response" property, and the Response property contains the actual Stream containing the data that was coming back from the service side (SAP-PI in this case). In my first implementation of the FirstChanceException and the static collection I was just saving the WebException itself for later analysis. But I found that by the time the ProtocolException had actually been thrown, the WebException.Response.Stream had already been read and discarded, so the data was no longer available to me.
FORTUNATELY, the Stream's CanSeek property is true, so what I did in the FirstChanceException event handler is to save the current position of the Stream object, then read and save the contents of the Stream (the data coming straight back from SAP-PI; Mime type of text/xml, and it was the actual SOAP envelope in all its glory).
Then after I had captured and stored away the data from the Stream, I re-positioned the Stream object back to its original position. This ensured that the code that was executed soon afterwards could still get to the original Stream data and do its thing.
Now one question I will toss out in hopes one of you might have an idea about.... surely there must be some place where I could get my hands on that underlying WebException without having to go all the way down to the AppDomain instance as I have done. Any ideas where?
And I wonder why the WCF developers chose to essentially consume the underlying WebException and deny us app programmers the chance to see that SOAP fault?
Your thoughts?
Hi Brad
I am currently on vacation, but I read you comment with great interest and will investigate your question #1 when I turn on the computer in earnest again :).
With regards to your question #2 I am afraid that I do not know.
"And I wonder why the WCF developers chose to essentially consume the underlying WebException and deny us app programmers the chance to see that SOAP fault?"
Post a Comment