WCF normally offers all the extensibility you need, but recently when implementing a WCF REST client in .NET 4.0, I found that it seems like there is at least a little inconsistency in how WCF REST handles service faults compared to other WCF implementations.
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.
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.
Thoughts and implementation
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:
- WCF REST service with custom http header check in .NET 4
- WCF REST client using custom http headers
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, omitting the optional exceptionHandler Action delegate in the call, as well as using a method group shorthand we can squeeze the code down to a fairly long (unreadable) one-liner.
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.