When use standard webHttpBinding in WCF (e.g. for RESTful endpoints) you may face with the following problem: standard error handler WebErrorHandler which ships with WebHttpBehavior returns generic “Internal Server Error” message for any exception you throw in the WCF service. Or to be more precise it returns response message which contains exception message like this:
1: HTTP/1.1 500 Internal Server Error
2: ...
3: Content-Type: application/xml; charset=utf-8
4:
5: <Fault xmlns="http://schemas.microsoft.com/ws/2005/05/envelope/none">
6: <Code>
7: <Value>Receiver</Value>
8: <Subcode>
9: <Value xmlns:a="http://schemas.microsoft.com/net/2005/12/windowscommunicationfoundation/dispatcher">
10: a:InternalServiceFault
11: </Value>
12: </Subcode>
13: </Code>
14: <Reason>
15: <Text xml:lang="en-US">
16: Product with the same Name already exists
17: </Text>
18: </Reason>
19: <Detail>
20: <ExceptionDetail xmlns="http://schemas.datacontract.org/2004/07/System.ServiceModel" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
21: <HelpLink i:nil="true"/>
22: <InnerException i:nil="true"/>
23: <Message>
24: Product with the same Name already exists
25: </Message>
26: <StackTrace>
27: at WcfService.Save(Product p)
28: at SyncInvokeSave(Object , Object[] , Object[] )
29: at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs)
30: at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc)
31: at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc& rpc)
32: at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(MessageRpc& rpc)
33: at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage3(MessageRpc& rpc)
34: at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage2(MessageRpc& rpc)
35: at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage1(MessageRpc& rpc)
36: at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)
37: </StackTrace>
38: <Type>
39: System.Exception
40: </Type>
41: </ExceptionDetail>
42: </Detail>
43: </Fault>
This message if very verbose and contains a lot of useless xml noise. In order to solve these problems simple custom error handler can be used:
1: public class CustomWebHttpErrorHandler : IErrorHandler
2: {
3: bool IErrorHandler.HandleError(Exception error)
4: {
5: return true;
6: }
7:
8: void IErrorHandler.ProvideFault(Exception error, MessageVersion version, ref Message fault)
9: {
10: var statusCode = HttpStatusCode.InternalServerError;
11: if (error is WebException)
12: {
13: statusCode = ((WebException) error).Status;
14: }
15: string body = ErrorBodyWriter.GetBody(statusCode, error.Message);
16: fault = Message.CreateMessage(version, null,
17: new ErrorBodyWriter {Code = statusCode, Message = error.Message});
18:
19: var rmp = new HttpResponseMessageProperty();
20: rmp.StatusCode = statusCode;
21: rmp.StatusDescription = string.Format("{0} {1}", statusCode, error.Message);
22: fault.Properties.Add(HttpResponseMessageProperty.Name, rmp);
23: }
24: }
Here I use 2 external classes: WebException from WcfRestContrib project and ErrorBodyWriter which realization I found in one of the forum threads. WebException is very easy: is just allows you to specify HTTP status when throw exception (as far as I know in .Net 4.0 there is standard equivalent of this class for WCF):
1: public class WebException : Exception
2: {
3: public WebException(HttpStatusCode status, string friendyMessage, params object[] args) :
4: base(string.Format(friendyMessage, args))
5: {
6: Status = status;
7: }
8:
9: public WebException(Exception innerException, HttpStatusCode status, string friendyMessage, params object[] args) :
10: base(string.Format(friendyMessage, args), innerException)
11: {
12: Status = status;
13: }
14:
15: public HttpStatusCode Status { get; private set; }
16:
17: public virtual void UpdateHeaders(WebHeaderCollection headers) { ... }
18: }
ErrorBodyWriter is used in order to construct POX-style response body:
1: internal class ErrorBodyWriter : BodyWriter
2: {
3: public HttpStatusCode Code { get; set; }
4: public string Message { get; set; }
5:
6: public ErrorBodyWriter()
7: : base(true)
8: {
9: }
10:
11: protected override void OnWriteBodyContents(System.Xml.XmlDictionaryWriter writer)
12: {
13: string errorMsg = GetBody(Code, Message);
14: System.Xml.Linq.XElement xElement = System.Xml.Linq.XElement.Load(new System.IO.StringReader(errorMsg));
15:
16: xElement.WriteTo(writer);
17: }
18:
19: public static string GetBody(HttpStatusCode statusCode, string msg)
20: {
21: string format = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
22: format += "<error>";
23: format += "<code>{0}</code>";
24: format += "<msg>{1}</msg>";
25: format += "</error>";
26:
27: string errorMsg = string.Format(format, statusCode, msg);
28: return errorMsg;
29: }
30: }
With this error handler you will have the following response body:
1: HTTP/1.1 500 InternalServerError: Product with the same name already exists
2: ...
3: Content-Type: application/xml; charset=utf-8
4:
5: <error><code>InternalServerError</code><msg>Product with the same name already exists</msg></error>
Note that instead of “Internal server error” we show actual error message also “InternalServerError: Product with the same name already exists”. We do this in the following line of code:
1: rmp.StatusDescription = string.Format("{0} {1}", statusCode, error.Message);
Client proxy generated by VS uses exactly StatusDescription for initialization of Exception.Message property. So now we will have error message at the client.
The remaining thing is to add this error handler into our service. I will show how to make it via web.config:
1: <system.serviceModel>
2: <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
3:
4: <extensions>
5: <behaviorExtensions>
6: <add name="customWebHttp" type="CustomWebHttpBehaviorExtention, WcfService, Version=1.0.0.0, Culture=neutral, PublicKeyToken=..." />
7:
8: </behaviorExtensions>
9: </extensions>
10:
11: <!-- bindings -->
12: <bindings>
13:
14: <webHttpBinding>
15: <binding name="webBinding">
16: </binding>
17: </webHttpBinding>
18:
19: </bindings>
20:
21: <!-- behaviors -->
22: <behaviors>
23:
24: <endpointBehaviors>
25: <!-- plain old XML with custom error handler -->
26: <behavior name="customPoxBehavior">
27: <customWebHttp/>
28: </behavior>
29: </endpointBehaviors>
30:
31: <serviceBehaviors>
32: <behavior name="defaultBehavior">
33: <serviceMetadata httpGetEnabled="true"/>
34: <serviceDebug includeExceptionDetailInFaults="true"/>
35: </behavior>
36: </serviceBehaviors>
37: </behaviors>
38:
39: <services>
40: <service name="MyWcfService" behaviorConfiguration="defaultBehavior">
41: <host>
42: <baseAddresses>
43: <add baseAddress="http://example.com/MyWcfService.svc" />
44: </baseAddresses>
45: </host>
46: <endpoint address="pox"
47: binding="webHttpBinding"
48: bindingConfiguration="webBinding"
49: behaviorConfiguration="customPoxBehavior"
50: contract="IMyWcfService" />
51: </service>
52: </services>
53:
54: </system.serviceModel>
Here we defined custom behavior extension with custom behavior (customPoxBehavior). The following classes are used for this:
1: public class CustomWebHttpBehavior : WebHttpBehavior
2: {
3: protected override void AddServerErrorHandlers(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
4: {
5: endpointDispatcher.ChannelDispatcher.ErrorHandlers.Clear();
6: endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(new CustomWebHttpErrorHandler());
7: }
8: }
9:
10: public class CustomWebHttpBehaviorExtention : BehaviorExtensionElement
11: {
12: public override Type BehaviorType
13: {
14: get { return typeof(CustomWebHttpBehavior); }
15: }
16:
17: protected override object CreateBehavior()
18: {
19: return new CustomWebHttpBehavior();
20: }
21: }
After that you will be able to use custom error handler for your WCF service.
Update 2011-06-12: with HTTPS this method can cause protocol violation exception. Here I posted solution for this problem.
Would this simple approach work for you?
ReplyDelete1. Do not throw generic exceptions from your WCF facade, use FaultException.
2. WebHttpBehavior.FaultExceptionEnabled = true
FaultException as well as FaultException with generic (blogspot trims generics :) ) are not catched on the client side in case of REST endpoints. It works only for SOAP unfortunately
ReplyDelete