WCF Extensibility – IErrorHandler
This post is part of a series about WCF extensibility points. For a list of all previous posts and planned future ones, go to the index page .
Whenever an operation throws an (unhandled) exception, the WCF runtime will catch that exception to prevent the whole service from going down. In most of the cases, that would mean a generic response being returned to the service (such as HTTP 500 Internal Server Error). If the exception happens to be a FaultException, or if the service has the debug setting to include exception detail in faults enabled (either using ServiceBehaviorAttribute.IncludeExceptionDetailInFaults or using the config setting in the <serviceDebug> element), then the response will contain a SOAP fault with the information in the exception. This works fine for most services, but there are cases where the application wants to customize the error which is returned to the client.
The IErrorHandler is the extension in WCF where the developer can, well, handle errors (exceptions) which occur in the application. It allows you to log the exception, and to customize the response message which will be sent back to the client when such error occurs. There are many usage examples of this interface (those are from the MSDN forums, a more general search would certainly yield more results), such as doing global error handling on a service, sending a better response to the client when it sends a badly serialized request, customizing the response for errors without using faults, even implementing a workaround for a bad bug in the ASP.NET AJAX support in 3.5. Error handlers are especially interesting on browser client scenarios, where we want the response for requests to be JavaScript-friendly (i.e., JSON, instead of XML/SOAP).
Public implementations in WCF
None. As with most runtime extensibility points, there are no public implementations in WCF. There are some internal implementations, mostly on the Web HTTP support (to be able to send errors back to the client in a format more REST / browser-friendly than SOAP).
Interface declaration
- public interface IErrorHandler
- {
- void ProvideFault(Exception error, MessageVersion version, ref Message fault);
- bool HandleError(Exception error);
- }
When an unhandled exception bubbles up from a service operation, ProvideFault is called for all error handlers registered in the list of handlers of the channel dispatcher (except for some “fatal” exceptions, such as stack overflows, out of memory, etc., in which case the service will be terminated). Each one will have the opportunity of inspecting the exception, and creating / modifying the “fault” to be returned to the client. The parameter name is actually misleading, since the message does not need to be a fault message. The MSDN documentation is misleading as well, as it says that it “enables the creation of a custom FaultException<TDetail> that is returned from an exception”. The returned “fault” can be any Message object, and it will be encoded and sent to the client as any other messages would.
HandleError is called after the message was returned to the client, and it’s used mostly for logging, notification and possibly shutting down the application. It’s also used to control whether a session in the server (if there is one) is to be terminated: if any of the error handlers returns true, then the exception is considered to be “contained”, and the session will be kept alive; if all error handlers return false (or if there is no custom error handler registered for the dispatcher) then the session is aborted. The instance context for the service is also aborted, if it’s not Single, if no error handlers can handle a given exception.
How to add error handlers
Error handlers only apply at the server side, and they are associated with the channel dispatchers. They’re typically defined in either service behaviors (for global error handling) or endpoint behaviors (for controlling the error response format for a specific endpoint type, such as web endpoints), as shown in the example below.
- public class MyBehavior : IEndpointBehavior
- {
- public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
- {
- endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(new MyErrorHandler());
- }
- }
Real world scenario: data annotation validation for JavaScript clients
I’ve seen quite a few of services where the first thing that each operation does is to validate that the input parameters have valid properties. A zip code has to follow certain regular expression, the balance a bank account should not be negative (at least in most banks), someone’s age should not be negative, etc. There is a great library for annotating class properties in the System.ComponentModel.DataAnnotations namespace, and it’s fairly simple to implement a simple validation using a parameter inspector for all the operations in the contract. In this example I have a simple HTML form application, and I’ll use an error handler to return the validation information in format which is easily accessible by the browser (i.e., in JSON).
And before starting with the code, the usual disclaimer: this is a sample for illustrating the topic of this post, this is not production-ready code. I tested it for a few contracts and it worked, but I cannot guarantee that it will work for all scenarios (please let me know if you find a bug or something missing). A more complete implementation would also include validation for simple types (which this one doesn’t), and it would definitely *not* use an in-memory dictionary as the “database” for storing the data. Also, the validation doesn’t go deep in the objects by default, so it currently it only validates the members of the parameters (not the members of the members).
I’ll start with the service. It’s a simple contact manager, where we can add, delete and query contacts
- [ServiceContract]
- public class ContactManager
- {
- static Dictionary<string, Contact> contacts = new Dictionary<string, Contact>();
- static int nextId = 0;
- [WebInvoke(Method = "POST", UriTemplate = "/Contact", ResponseFormat = WebMessageFormat.Json)]
- public string AddContact(Contact contact)
- {
- string id = (++nextId).ToString();
- contacts.Add(id, contact);
- string requestUri = WebOperationContext.Current.IncomingRequest.UriTemplateMatch.RequestUri.ToString();
- if (requestUri.EndsWith("/"))
- {
- requestUri = requestUri.Substring(0, requestUri.Length - 1);
- }
- WebOperationContext.Current.OutgoingResponse.Headers[HttpResponseHeader.Location] = requestUri + "s/" + id;
- return id;
- }
- [WebInvoke(Method = "DELETE", UriTemplate = "/Contact/{id}")]
- public void DeleteContact(string id)
- {
- if (contacts.ContainsKey(id))
- {
- contacts.Remove(id);
- }
- else
- {
- throw new WebFaultException(HttpStatusCode.NotFound);
- }
- }
- [WebGet(UriTemplate = "/Contacts/{id}", ResponseFormat = WebMessageFormat.Json)]
- public Contact GetContact(string id)
- {
- if (contacts.ContainsKey(id))
- {
- return contacts[id];
- }
- else
- {
- throw new WebFaultException(HttpStatusCode.NotFound);
- }
- }
- }
The Contact class is defined with some annotation for its data members. Both name and e-mail properties are required (i.e., they cannot be null), and their values must follow certain guidelines. The age is not required, but it’s limited to a certain range.
- [DataContract]
- public class Contact
- {
- [DataMember]
- [Required(ErrorMessage = "Name is required")]
- [StringLength(20, MinimumLength = 1, ErrorMessage = "Name must have between 1 and 20 characters")]
- public string Name { get; set; }
- [DataMember]
- [Range(0, 150, ErrorMessage = "Age must be an integer between 0 and 150")]
- public int Age { get; set; }
- [DataMember]
- [Required(ErrorMessage = "E-mail is required")]
- [RegularExpression(@"[^\@]+\@[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)+", ErrorMessage = "E-mail is invalid")]
- public string Email { get; set; }
- }
Now to add the validation logic. Since this is a web endpoint, I’ll create a subclass of WebHttpBehavior which has some nice overrides I can use for that. The behavior must add two elements: a parameter inspector which will validate the incoming data, and the new error handler which will return the errors in a nice format.
- public class WebHttpWithValidationBehavior : WebHttpBehavior
- {
- protected override void AddServerErrorHandlers(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
- {
- int errorHandlerCount = endpointDispatcher.ChannelDispatcher.ErrorHandlers.Count;
- base.AddServerErrorHandlers(endpoint, endpointDispatcher);
- IErrorHandler webHttpErrorHandler = endpointDispatcher.ChannelDispatcher.ErrorHandlers[errorHandlerCount];
- endpointDispatcher.ChannelDispatcher.ErrorHandlers.RemoveAt(errorHandlerCount);
- ValidationAwareErrorHandler newHandler = new ValidationAwareErrorHandler(webHttpErrorHandler);
- endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(newHandler);
- }
- public override void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
- {
- base.ApplyDispatchBehavior(endpoint, endpointDispatcher);
- foreach (DispatchOperation operation in endpointDispatcher.DispatchRuntime.Operations)
- {
- operation.ParameterInspectors.Add(new ValidatingParameterInspector());
- }
- }
- }
The inspector is fairly simple, it simply calls the Validator.ValidateObject on every non-null parameter which is passed to the function. This will scan the data annotation properties and validate the instance against them.
- public class ValidatingParameterInspector : IParameterInspector
- {
- public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState)
- {
- }
- public object BeforeCall(string operationName, object[] inputs)
- {
- foreach (var input in inputs)
- {
- if (input != null)
- {
- ValidationContext context = new ValidationContext(input, null, null);
- Validator.ValidateObject(input, context, true);
- }
- }
- return null;
- }
- }
The error handler is the piece responsible for sending the validation error in a form that the client understands. In this implementation, it creates a new message object if the exception is a ValidationException (which is thrown when the ValidateObject call fails). If the exception is something else, it will simply delegate to the original error handler added by the WebHttpBehavior. The new message created for validation errors will use the Json encoder from the WebMessageEncodingBindingElement (guided by the WebBodyFormatMessageProperty in the message). And the body of the message is written using the mapping between XML and JSON to create the expected output.
- public class ValidationAwareErrorHandler : IErrorHandler
- {
- IErrorHandler originalErrorHandler;
- public ValidationAwareErrorHandler(IErrorHandler originalErrorHandler)
- {
- this.originalErrorHandler = originalErrorHandler;
- }
- public bool HandleError(Exception error)
- {
- return error is ValidationException || this.originalErrorHandler.HandleError(error);
- }
- public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
- {
- ValidationException validationException = error as ValidationException;
- if (validationException != null)
- {
- fault = Message.CreateMessage(version, null, new ValidationErrorBodyWriter(validationException));
- HttpResponseMessageProperty prop = new HttpResponseMessageProperty();
- prop.StatusCode = HttpStatusCode.BadRequest;
- prop.Headers[HttpResponseHeader.ContentType] = "application/json; charset=utf-8";
- fault.Properties.Add(HttpResponseMessageProperty.Name, prop);
- fault.Properties.Add(WebBodyFormatMessageProperty.Name, new WebBodyFormatMessageProperty(WebContentFormat.Json));
- }
- else
- {
- this.originalErrorHandler.ProvideFault(error, version, ref fault);
- }
- }
- class ValidationErrorBodyWriter : BodyWriter
- {
- private ValidationException validationException;
- Encoding utf8Encoding = new UTF8Encoding(false);
- public ValidationErrorBodyWriter(ValidationException validationException)
- : base(true)
- {
- this.validationException = validationException;
- }
- protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
- {
- writer.WriteStartElement("root");
- writer.WriteAttributeString("type", "object");
- writer.WriteStartElement("ErrorMessage");
- writer.WriteAttributeString("type", "string");
- writer.WriteString(this.validationException.ValidationResult.ErrorMessage);
- writer.WriteEndElement();
- writer.WriteStartElement("MemberNames");
- writer.WriteAttributeString("type", "array");
- foreach (var member in this.validationException.ValidationResult.MemberNames)
- {
- writer.WriteStartElement("item");
- writer.WriteAttributeString("type", "string");
- writer.WriteString(member);
- writer.WriteEndElement();
- }
- writer.WriteEndElement();
- writer.WriteEndElement();
- }
- }
- }
The service itself is done. In order to hook up the endpoint behavior to the service creation, I’m using a custom service host factory (see more about it in the next post), so that I don’t need to worry about configuration extensions.
- <%@ ServiceHost Language="C#" Debug="true" Service="ParameterValidation.ContactManager" CodeBehind="ContactManager.svc.cs" Factory="ParameterValidation.ContactManagerFactory" %>
- using System;
- using System.ServiceModel;
- using System.ServiceModel.Activation;
- using System.ServiceModel.Description;
- namespace ParameterValidation
- {
- public class ContactManagerFactory : ServiceHostFactory
- {
- protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
- {
- ServiceHost host = new ServiceHost(serviceType, baseAddresses);
- ServiceEndpoint endpoint = host.AddServiceEndpoint(serviceType, new WebHttpBinding(), "");
- endpoint.Behaviors.Add(new WebHttpWithValidationBehavior());
- return host;
- }
- }
- }
Now that everything is set up, we can test the service. I like to use a unit test framework (when I was writing this sample I wrote the tests first), and qUnit is one of my favorites. Below is a snippet of the tests which I used to verify the service code.
- module("Validation tests");
- asyncTest("Missing name", 2, function () {
- sendAndExpectError(undefined, 30, "john@doe.com", 400, "Name is required");
- });
- asyncTest("Negative age", 2, function () {
- sendAndExpectError("John Doe", -1, "john@doe.com", 400, "Age must be an integer between 0 and 150");
- });
- asyncTest("Very high age", 2, function () {
- sendAndExpectError("John Doe", 151, "john@doe.com", 400, "Age must be an integer between 0 and 150");
- });
- asyncTest("Missing e-mail", 2, function () {
- sendAndExpectError("John Doe", 30, undefined, 400, "E-mail is required");
- });
- asyncTest("Invalid e-mail", 2, function () {
- sendAndExpectError("John Doe", 30, "abcdef", 400, "E-mail is invalid");
- });
- function sendAndExpectError(name, age, email, expectedStatusCode, expectedMessage) {
- var data = JSON.stringify({ Name: name, Age: age, Email: email });
- $.ajax({
- type: "POST",
- url: serviceAddress + "Contact",
- contentType: "application/json",
- data: data,
- success: function (data) {
- ok(false, "Validation should have failed the request");
- ok(false, "Result: " + data);
- },
- error: function (jqXHR) {
- var statusCode = jqXHR.status;
- var response = $.parseJSON(jqXHR.responseText); ;
- equal(statusCode, expectedStatusCode, "Status code is correct");
- equal(response.ErrorMessage, expectedMessage, "Message is correct");
- },
- complete: function () {
- start();
- }
- });
- }
And that’s it. In the code for this post I added a simple HTML form which uses this service as well.
Coming up
One of my personal favorites: service host factories, or how to do everything in IIS without ever needing configuration (web.config).
Comments
- Anonymous
September 11, 2011
Hi Carlos, I have implemented the custom errorhandler as explained by the above post. It is working fine for the XML end points. But I am getting the XML parsing error for the Json end point and content type is set as application/xml in the response header even though i am explicitly setting it as application/json in the errorhhandler. Is there any thing i am missing here. - Anonymous
September 11, 2011
Is this error on incoming or outgoing messages? If I understand correctly, it's on outgoing ones. To get a message to be written in JSON you need to do two things: set the content-type to "application/json", and set the WebBodyFormatMessageProperty in the message to Json. - Anonymous
September 12, 2011
Hi Carlos, Here is the code for errorhandler implementation using framework 3.5. I am gettign the correct response for XML end point but I am getting page not found in the browser for the Json end point. Please let me know is there any thing i am missing here.Here is the configuration for the service<system.serviceModel> <serviceHostingEnvironment aspNetCompatibilityEnabled="true" /> <extensions> <behaviorExtensions> <add name="enhancedWebHttp" type="Licensing.Services.EnhancedWebHttpElement, Licensing.Services, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/> </behaviorExtensions> </extensions> <bindings> <webHttpBinding> <binding name="webBinding"> </binding> </webHttpBinding> </bindings> <behaviors> <endpointBehaviors> <behavior name="jsonBehavior"> <enhancedWebHttp defaultOutgoingResponseFormat="Json"/> </behavior> <behavior name="poxBehavior"> <enhancedWebHttp/> </behavior> </endpointBehaviors> <serviceBehaviors> <behavior name="Licensing.Services.restserviceBehavior" > <!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment --> <serviceMetadata httpGetEnabled="true"/> <!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information --> <serviceDebug includeExceptionDetailInFaults="false"/> </behavior> </serviceBehaviors> </behaviors> <services> <service name="Licensing.Services.restservice" behaviorConfiguration="Licensing.Services.restserviceBehavior"> <host> <baseAddresses> <add baseAddress="http://localhost/LicensingAPIService/restservice.svc"/> </baseAddresses> </host> <endpoint address="json" binding="webHttpBinding" bindingConfiguration="webBinding" behaviorConfiguration="jsonBehavior"
<endpoint address="pox" binding="webHttpBinding" bindingConfiguration="webBinding" behaviorConfiguration="poxBehavior"contract="Licensing.Services.IRestService" />
</service> </services> </system.serviceModel>Here is the configuration for EnhancedWebHttpElementpublic sealed class EnhancedWebHttpElement : BehaviorExtensionElement { #region Type specific properties [ConfigurationProperty("defaultBodyStyle", DefaultValue = WebMessageBodyStyle.Bare)] public WebMessageBodyStyle DefaultBodyStyle { get { return (WebMessageBodyStyle)this["defaultBodyStyle"]; } set { this["defaultBodyStyle"] = value; } } [ConfigurationProperty("defaultOutgoingRequestFormat", DefaultValue = WebMessageFormat.Xml)] public WebMessageFormat DefaultOutgoingRequestFormat { get { return (WebMessageFormat)this["defaultOutgoingRequestFormat"]; } set { this["defaultOutgoingRequestFormat"] = value; } } [ConfigurationProperty("defaultOutgoingResponseFormat", DefaultValue = WebMessageFormat.Xml)] public WebMessageFormat DefaultOutgoingResponseFormat { get { return (WebMessageFormat)this["defaultOutgoingResponseFormat"]; } set { this["defaultOutgoingResponseFormat"] = value; } } #endregion #region Base class overrides protected override object CreateBehavior() { LicensingBehavior result = new LicensingBehavior(); result.DefaultBodyStyle = this.DefaultBodyStyle; result.DefaultOutgoingRequestFormat = this.DefaultOutgoingRequestFormat; result.DefaultOutgoingResponseFormat = this.DefaultOutgoingResponseFormat; return result; } public override Type BehaviorType { get { return typeof(LicensingBehavior); } } #endregion }Here is the code for LicensingBehaviorpublic class LicensingBehavior : WebHttpBehavior { protected override void AddServerErrorHandlers(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { int errorHandlerCount = endpointDispatcher.ChannelDispatcher.ErrorHandlers.Count; base.AddServerErrorHandlers(endpoint, endpointDispatcher); IErrorHandler webHttpErrorHandler = endpointDispatcher.ChannelDispatcher.ErrorHandlers[errorHandlerCount]; endpointDispatcher.ChannelDispatcher.ErrorHandlers.RemoveAt(errorHandlerCount); RestErrorHandler newHandler = new RestErrorHandler(webHttpErrorHandler,DefaultOutgoingResponseFormat); endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(newHandler); } }Here si the code for ErrorHandlerpublic class RestErrorHandler : IErrorHandler { IErrorHandler _originalErrorHandler; WebMessageFormat _format; public RestErrorHandler(IErrorHandler originalErrorHandler,WebMessageFormat format) { this._originalErrorHandler = originalErrorHandler; this._format = format; } public bool HandleError(Exception error) { return error is WebProtocolException; } public void ProvideFault(Exception error, MessageVersion version, ref Message fault) { WebProtocolException licensingException = error as WebProtocolException; if (licensingException != null) { fault = Message.CreateMessage(version, null, new ValidationErrorBodyWriter(licensingException)); if (_format == WebMessageFormat.Json) { HttpResponseMessageProperty prop = new HttpResponseMessageProperty(); prop.StatusCode = licensingException.StatusCode; prop.Headers[HttpResponseHeader.ContentType] = "application/json; charset=utf-8"; fault.Properties.Add(HttpResponseMessageProperty.Name, prop); fault.Properties.Add(WebBodyFormatMessageProperty.Name, new WebBodyFormatMessageProperty(WebContentFormat.Json)); } else if(_format == WebMessageFormat.Xml) { HttpResponseMessageProperty prop = new HttpResponseMessageProperty(); prop.StatusCode = licensingException.StatusCode; prop.Headers[HttpResponseHeader.ContentType] = "application/xml; charset=utf-8"; fault.Properties.Add(HttpResponseMessageProperty.Name, prop); fault.Properties.Add(WebBodyFormatMessageProperty.Name, new WebBodyFormatMessageProperty(WebContentFormat.Xml)); } } else { this._originalErrorHandler.ProvideFault(error, version, ref fault); } } class ValidationErrorBodyWriter : BodyWriter { private WebProtocolException validationException; Encoding utf8Encoding = new UTF8Encoding(false); public ValidationErrorBodyWriter(WebProtocolException validationException) : base(true) { this.validationException = validationException; } protected override void OnWriteBodyContents(XmlDictionaryWriter writer) { writer.WriteStartElement("root"); writer.WriteAttributeString("type", "object"); writer.WriteStartElement("StatusCode"); writer.WriteAttributeString("type", "string"); writer.WriteString(this.validationException.StatusCode.ToString()); writer.WriteEndElement(); writer.WriteStartElement("Description"); writer.WriteAttributeString("type", "string"); writer.WriteString(this.validationException.StatusDescription); writer.WriteEndElement(); writer.WriteEndElement(); } } } contract="Licensing.Services.IRestService"/>
- Anonymous
September 13, 2011
I'm a bit confused... what actually runs:public class ContactManagerFactory : ServiceHostFactoryI just cannot get this to break into the inspector. I have a hunch it's not running the code. - Anonymous
September 18, 2011
Nayangowda, if you add a breakpoint in your error handler code, is it being hit? Is the codepath for the JSON response being executed as expected? - Anonymous
September 18, 2011
Coder, having breakpoints in the code inside the .svc file is tricky - often once the .svc file is loaded, the ASP.NET process is started at that point and the code is executed right away, so you don't have time to attach to it to get the breakpoint set. A few things you can try: throw an exception in CreateServiceHost to see if it is actually being invoked or not (if you see the exception when you try to browse to the .svc file, then you'll know that it's being run). Another thing you can try is to move the factory code into the rest of the project itself, then if you're using F5 in Visual Studio, you should be able to hit the breakpoints easier. - Anonymous
September 18, 2011
Hi Carlos, I placed the breakpoint in the Errorhandler and the codepath is being executed as expected. I am not using the factory in this case. - Anonymous
September 19, 2011
Nayangowda, another thing to check is to see whether there are any other error handlers being executed after yours (which can be resetting the message). - Anonymous
September 20, 2011
Carlos, Not sure... Is there a way i can figure out if there is any other error handler gettign executed after mine? - Anonymous
November 04, 2011
Hi Carlos,I wonder how to implemet similar behavior for basicHttpBinding and netTcpBinding? - Anonymous
November 05, 2011
Hi miksh, for SOAP-based bindings it's even simpler, since you don't have to create the response yourself - you can use FaultException's for that. I've posted a solution at github.com/.../ParameterValidationWithSoap (if you go one directory up you'll find the original sample). - Anonymous
December 07, 2011
Hi carlos, how can i implement where in after i log the error i will throwing exception error?liketry{....}catch(Exception e){//code heree = null;} - Anonymous
December 08, 2011
The comment has been removed - Anonymous
June 24, 2012
Nayangowda did you ever find a resolution to your problem? We're having the same issue here!Many Thanks