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

  1. public interface IErrorHandler
  2. {
  3.     void ProvideFault(Exception error, MessageVersion version, ref Message fault);
  4.     bool HandleError(Exception error);
  5. }

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.

  1. public class MyBehavior : IEndpointBehavior
  2. {
  3.     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  4.     {
  5.         endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(new MyErrorHandler());
  6.     }
  7. }

 

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

  1. [ServiceContract]
  2. public class ContactManager
  3. {
  4.     static Dictionary<string, Contact> contacts = new Dictionary<string, Contact>();
  5.     static int nextId = 0;
  6.  
  7.     [WebInvoke(Method = "POST", UriTemplate = "/Contact", ResponseFormat = WebMessageFormat.Json)]
  8.     public string AddContact(Contact contact)
  9.     {
  10.         string id = (++nextId).ToString();
  11.         contacts.Add(id, contact);
  12.         string requestUri = WebOperationContext.Current.IncomingRequest.UriTemplateMatch.RequestUri.ToString();
  13.         if (requestUri.EndsWith("/"))
  14.         {
  15.             requestUri = requestUri.Substring(0, requestUri.Length - 1);
  16.         }
  17.  
  18.         WebOperationContext.Current.OutgoingResponse.Headers[HttpResponseHeader.Location] = requestUri + "s/" + id;
  19.         return id;
  20.     }
  21.  
  22.     [WebInvoke(Method = "DELETE", UriTemplate = "/Contact/{id}")]
  23.     public void DeleteContact(string id)
  24.     {
  25.         if (contacts.ContainsKey(id))
  26.         {
  27.             contacts.Remove(id);
  28.         }
  29.         else
  30.         {
  31.             throw new WebFaultException(HttpStatusCode.NotFound);
  32.         }
  33.     }
  34.  
  35.     [WebGet(UriTemplate = "/Contacts/{id}", ResponseFormat = WebMessageFormat.Json)]
  36.     public Contact GetContact(string id)
  37.     {
  38.         if (contacts.ContainsKey(id))
  39.         {
  40.             return contacts[id];
  41.         }
  42.         else
  43.         {
  44.             throw new WebFaultException(HttpStatusCode.NotFound);
  45.         }
  46.     }
  47. }

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.

  1. [DataContract]
  2. public class Contact
  3. {
  4.     [DataMember]
  5.     [Required(ErrorMessage = "Name is required")]
  6.     [StringLength(20, MinimumLength = 1, ErrorMessage = "Name must have between 1 and 20 characters")]
  7.     public string Name { get; set; }
  8.  
  9.     [DataMember]
  10.     [Range(0, 150, ErrorMessage = "Age must be an integer between 0 and 150")]
  11.     public int Age { get; set; }
  12.  
  13.     [DataMember]
  14.     [Required(ErrorMessage = "E-mail is required")]
  15.     [RegularExpression(@"[^\@]+\@[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)+", ErrorMessage = "E-mail is invalid")]
  16.     public string Email { get; set; }
  17. }

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.

  1. public class WebHttpWithValidationBehavior : WebHttpBehavior
  2. {
  3.     protected override void AddServerErrorHandlers(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  4.     {
  5.         int errorHandlerCount = endpointDispatcher.ChannelDispatcher.ErrorHandlers.Count;
  6.         base.AddServerErrorHandlers(endpoint, endpointDispatcher);
  7.         IErrorHandler webHttpErrorHandler = endpointDispatcher.ChannelDispatcher.ErrorHandlers[errorHandlerCount];
  8.         endpointDispatcher.ChannelDispatcher.ErrorHandlers.RemoveAt(errorHandlerCount);
  9.         ValidationAwareErrorHandler newHandler = new ValidationAwareErrorHandler(webHttpErrorHandler);
  10.         endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(newHandler);
  11.     }
  12.  
  13.     public override void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  14.     {
  15.         base.ApplyDispatchBehavior(endpoint, endpointDispatcher);
  16.         foreach (DispatchOperation operation in endpointDispatcher.DispatchRuntime.Operations)
  17.         {
  18.             operation.ParameterInspectors.Add(new ValidatingParameterInspector());
  19.         }
  20.     }
  21. }

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.

  1. public class ValidatingParameterInspector : IParameterInspector
  2. {
  3.     public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState)
  4.     {
  5.     }
  6.  
  7.     public object BeforeCall(string operationName, object[] inputs)
  8.     {
  9.         foreach (var input in inputs)
  10.         {
  11.             if (input != null)
  12.             {
  13.                 ValidationContext context = new ValidationContext(input, null, null);
  14.                 Validator.ValidateObject(input, context, true);
  15.             }
  16.         }
  17.  
  18.         return null;
  19.     }
  20. }

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.

  1. public class ValidationAwareErrorHandler : IErrorHandler
  2. {
  3.     IErrorHandler originalErrorHandler;
  4.     public ValidationAwareErrorHandler(IErrorHandler originalErrorHandler)
  5.     {
  6.         this.originalErrorHandler = originalErrorHandler;
  7.     }
  8.  
  9.     public bool HandleError(Exception error)
  10.     {
  11.         return error is ValidationException || this.originalErrorHandler.HandleError(error);
  12.     }
  13.  
  14.     public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
  15.     {
  16.         ValidationException validationException = error as ValidationException;
  17.         if (validationException != null)
  18.         {
  19.             fault = Message.CreateMessage(version, null, new ValidationErrorBodyWriter(validationException));
  20.             HttpResponseMessageProperty prop = new HttpResponseMessageProperty();
  21.             prop.StatusCode = HttpStatusCode.BadRequest;
  22.             prop.Headers[HttpResponseHeader.ContentType] = "application/json; charset=utf-8";
  23.             fault.Properties.Add(HttpResponseMessageProperty.Name, prop);
  24.             fault.Properties.Add(WebBodyFormatMessageProperty.Name, new WebBodyFormatMessageProperty(WebContentFormat.Json));
  25.         }
  26.         else
  27.         {
  28.             this.originalErrorHandler.ProvideFault(error, version, ref fault);
  29.         }
  30.     }
  31.  
  32.     class ValidationErrorBodyWriter : BodyWriter
  33.     {
  34.         private ValidationException validationException;
  35.         Encoding utf8Encoding = new UTF8Encoding(false);
  36.  
  37.         public ValidationErrorBodyWriter(ValidationException validationException)
  38.             : base(true)
  39.         {
  40.             this.validationException = validationException;
  41.         }
  42.  
  43.         protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
  44.         {
  45.             writer.WriteStartElement("root");
  46.             writer.WriteAttributeString("type", "object");
  47.  
  48.             writer.WriteStartElement("ErrorMessage");
  49.             writer.WriteAttributeString("type", "string");
  50.             writer.WriteString(this.validationException.ValidationResult.ErrorMessage);
  51.             writer.WriteEndElement();
  52.  
  53.             writer.WriteStartElement("MemberNames");
  54.             writer.WriteAttributeString("type", "array");
  55.             foreach (var member in this.validationException.ValidationResult.MemberNames)
  56.             {
  57.                 writer.WriteStartElement("item");
  58.                 writer.WriteAttributeString("type", "string");
  59.                 writer.WriteString(member);
  60.                 writer.WriteEndElement();
  61.             }
  62.             writer.WriteEndElement();
  63.  
  64.             writer.WriteEndElement();
  65.         }
  66.     }
  67. }

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.

  1. <%@ ServiceHost Language="C#" Debug="true" Service="ParameterValidation.ContactManager" CodeBehind="ContactManager.svc.cs" Factory="ParameterValidation.ContactManagerFactory" %>
  2.  
  3. using System;
  4. using System.ServiceModel;
  5. using System.ServiceModel.Activation;
  6. using System.ServiceModel.Description;
  7.  
  8. namespace ParameterValidation
  9. {
  10.     public class ContactManagerFactory : ServiceHostFactory
  11.     {
  12.         protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
  13.         {
  14.             ServiceHost host = new ServiceHost(serviceType, baseAddresses);
  15.             ServiceEndpoint endpoint = host.AddServiceEndpoint(serviceType, new WebHttpBinding(), "");
  16.             endpoint.Behaviors.Add(new WebHttpWithValidationBehavior());
  17.             return host;
  18.         }
  19.     }
  20. }

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.

  1. module("Validation tests");
  2.  
  3. asyncTest("Missing name", 2, function () {
  4.     sendAndExpectError(undefined, 30, "john@doe.com", 400, "Name is required");
  5. });
  6.  
  7. asyncTest("Negative age", 2, function () {
  8.     sendAndExpectError("John Doe", -1, "john@doe.com", 400, "Age must be an integer between 0 and 150");
  9. });
  10.  
  11. asyncTest("Very high age", 2, function () {
  12.     sendAndExpectError("John Doe", 151, "john@doe.com", 400, "Age must be an integer between 0 and 150");
  13. });
  14.  
  15. asyncTest("Missing e-mail", 2, function () {
  16.     sendAndExpectError("John Doe", 30, undefined, 400, "E-mail is required");
  17. });
  18.  
  19. asyncTest("Invalid e-mail", 2, function () {
  20.     sendAndExpectError("John Doe", 30, "abcdef", 400, "E-mail is invalid");
  21. });
  22.  
  23. function sendAndExpectError(name, age, email, expectedStatusCode, expectedMessage) {
  24.     var data = JSON.stringify({ Name: name, Age: age, Email: email });
  25.  
  26.     $.ajax({
  27.         type: "POST",
  28.         url: serviceAddress + "Contact",
  29.         contentType: "application/json",
  30.         data: data,
  31.         success: function (data) {
  32.             ok(false, "Validation should have failed the request");
  33.             ok(false, "Result: " + data);
  34.         },
  35.         error: function (jqXHR) {
  36.             var statusCode = jqXHR.status;
  37.             var response = $.parseJSON(jqXHR.responseText); ;
  38.             equal(statusCode, expectedStatusCode, "Status code is correct");
  39.             equal(response.ErrorMessage, expectedMessage, "Message is correct");
  40.         },
  41.         complete: function () {
  42.             start();
  43.         }
  44.     });
  45. }

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).

[Code in this post]

[Back to the index]