views:

1389

answers:

4

Short Version: Is there a/what is the suggested way to return error details to the client when an exception is thrown in an AJAX-Enabled WCF Service (aside from just throwing the gates open and sending back all of the exception details)?

Long Version:

I've got a relatively simple AJAX-enabled WCF service that I'm calling from the client using the default service proxy. I've provided code snippets below, but I do not believe there is anything wrong with the code per se.

My problem is that if I throw an exception in the service, the error object returned to the client is always generic:

{
    "ExceptionDetail":null,
    "ExceptionType":null,
    "Message":"The server was unable to process the request..."
    "StackTrace":null
}

Ideally I would like to display different error messages on the client depending on what went wrong.

One option is to allow exceptions in WCF faults, which would provide me with the full stack trace and everything, but I appreciate the security concerns with this, and that's actually a lot more information than I need. I could make do with just being able to send back a string describing the problem or something, but I don't see a way to do this.

My Service Code:

[ServiceContract(Namespace = "MyNamespace")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class MyService
{
    [OperationContract]
    public void DoStuff(string param1, string etc)
    {
        //Do some stuff that maybe causes an exception
    }
}

On the client:

MyNamespace.MyService.DoStuff(
    param1,
    etc,
    function() { alert("success"); },
    HandleError);

where "HandleError" is just a generic error handling method that would display details about the error.

A: 

It looks like the preferred way to differentiate between error states is via the http status code on the response. This can be manually set in the service method, but I'm not sure if this is the best way to approach this or not:

[ServiceContract(Namespace = "MyNamespace")]
AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class MyService
{
    [OperationContract]
    public void DoStuff(string param1, string etc)
    {
        try
        {
        //Do some stuff that maybe causes an exception
        }
        catch(ExceptionType1 ex)
        {
            WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.BadRequest;
        }
        catch(ExceptionType2 ex)
        {
            WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.Conflict;
        }
        ... etc.
    }
}
John Price
A: 

I would suggest wrapping the exception and letting all exceptions go through the service wrapped. The ones that you expect you will filter out (like in the above example) with meaningful messages. The general case will just say:

throw new ApplicationException("Unknown Error");

This way you won't give out information to the client about the inner workings of the service but will be able to show meaningful messages for cases where you need to pass back error information to the client like security exceptions, etc.

Stilgar
+5  A: 

EDIT: Updated the post with a proper custom json error handler

The quick but non-preffered way.

<serviceDebug includeExceptionDetailInFaults="true"/>

In Your service behavior will give you all the details you need.

The Nice way

All exceptions from the application are converted to an JsonError and serialized using a DataContractJsonSerializer. The Exception.Message is used directly. FaultExceptions provide the FaultCode and other exception are threated as unknown with faultcode -1.

FaultException are sent with HTTP status code 400 and other exceptions are HTTP code 500 - internal server error. This not nescessary as the faultcode can be used to decide if it is and unknown error. It was however convenient in my app.

The error handler

internal class CustomErrorHandler : IErrorHandler
{
    public bool HandleError(Exception error)
    {
     //Tell the system that we handle all errors here.
     return true;
    }

    public void ProvideFault(Exception error, System.ServiceModel.Channels.MessageVersion version, ref System.ServiceModel.Channels.Message fault)
    {
     if (error is FaultException<int>)
     {
      FaultException<int> fe = (FaultException<int>)error;

      //Detail for the returned value
      int faultCode = fe.Detail;
      string cause = fe.Message;

      //The json serializable object
      JsonError msErrObject = new JsonError { Message = cause, FaultCode = faultCode };

      //The fault to be returned
      fault = Message.CreateMessage(version, "", msErrObject, new DataContractJsonSerializer(msErrObject.GetType()));

      // tell WCF to use JSON encoding rather than default XML
      WebBodyFormatMessageProperty wbf = new WebBodyFormatMessageProperty(WebContentFormat.Json);

      // Add the formatter to the fault
      fault.Properties.Add(WebBodyFormatMessageProperty.Name, wbf);

      //Modify response
      HttpResponseMessageProperty rmp = new HttpResponseMessageProperty();

      // return custom error code, 400.
      rmp.StatusCode = System.Net.HttpStatusCode.BadRequest;
      rmp.StatusDescription = "Bad request";

      //Mark the jsonerror and json content
      rmp.Headers[HttpResponseHeader.ContentType] = "application/json";
      rmp.Headers["jsonerror"] = "true";

      //Add to fault
      fault.Properties.Add(HttpResponseMessageProperty.Name, rmp);
     }
     else
     {
      //Arbitraty error
      JsonError msErrObject = new JsonError { Message = error.Message, FaultCode = -1};

      // create a fault message containing our FaultContract object
      fault = Message.CreateMessage(version, "", msErrObject, new DataContractJsonSerializer(msErrObject.GetType()));

      // tell WCF to use JSON encoding rather than default XML
      var wbf = new WebBodyFormatMessageProperty(WebContentFormat.Json);
      fault.Properties.Add(WebBodyFormatMessageProperty.Name, wbf);

      //Modify response
      var rmp = new HttpResponseMessageProperty();

      rmp.Headers[HttpResponseHeader.ContentType] = "application/json";
      rmp.Headers["jsonerror"] = "true";

      //Internal server error with exception mesasage as status.
      rmp.StatusCode = System.Net.HttpStatusCode.InternalServerError;
      rmp.StatusDescription = error.Message;

      fault.Properties.Add(HttpResponseMessageProperty.Name, rmp);
     }
    }

    #endregion
}

Webbehaviour used to install the above error handler

internal class AddErrorHandlerBehavior : WebHttpBehavior
{
    protected override void AddServerErrorHandlers(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
     base.AddServerErrorHandlers(endpoint, endpointDispatcher);

     //Remove all other error handlers
     endpointDispatcher.ChannelDispatcher.ErrorHandlers.Clear();
     //Add our own
     endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(new CustomErrorHandler());
    }
}

The json error data contract

Specifies the json error format. Add properties here to change the error format.

[DataContractFormat]
public class JsonError
{
    [DataMember]
    public string Message { get; set; }

    [DataMember]
    public int FaultCode { get; set; }
}

Using the error handler

Self-hosted

ServiceHost wsHost = new ServiceHost(new Webservice1(), new Uri("http://localhost/json")); 

ServiceEndpoint wsEndpoint = wsHost.AddServiceEndpoint(typeof(IWebservice1), new WebHttpBinding(), string.Empty);

wsEndpoint.Behaviors.Add(new AddErrorHandlerBehavior());

App.config

<extensions>  
  <behaviorExtensions>  
    <add name="errorHandler"  
     type="WcfServiceLibrary1.ErrorHandlerElement, WcfServiceLibrary1" />  
  </behaviorExtensions>  
</extensions>
Fedearne
Most of the articles I've read seem to frown upon including exception details in production settings since it's exposing information about your service's implementation.That said, none of those same articles seem to offer a "better" solution to returning error information.
John Price
It should be noted that there is a bug in WCF that when adding the assembly information to the behaviorExtension tag, you need to use the fully qualified name (tack on `, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null` if the code is in the same assembly as your services).
Justin Niessner
A: 

The only way I am able to get exception detail back is with

<serviceDebug includeExceptionDetailInFaults="true"/>

I tried the suggested method with extending HttpWebBehavior but when it is used with enableWebScript like the following:

<behavior name="JsonBehavior">
    <myWebHttp/>
    <enableWebScript/>
</behavior>

The WCF call will return status code 202 without anything information. If the configuration turns into

<behavior name="JsonBehavior">
        <enableWebScript/>
        <myWebHttp/>
    </behavior>

You will get the nicely formatted message back but you lose all the json formatting functionality as well as request parameter parsing from enableWebScript, which completely defeats the purpose of using enableWebScript

I have attempted FaultContract as well but it appears to only work for Service References and not AJAX calls from JQuery.

It would be nice to be able to override WebScriptEnablingBehavior or the service itself to provide custom error handling.