Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

jQuery success callback called with empty response when WCF method throws an Exception

I'm tearing my hair out over this one, so bear with me (it's a long post).

Basic Info

  • ASP.NET 3.5 with WCF service in ASP.NET compatibility mode
  • Using jQuery with this service proxy for AJAX requests
  • Custom IErrorHandler and IServiceBehavior implementation to trap exceptions and provide Faults, which are serialized to JSON
  • I'm testing locally using Cassini (I've seen some threads that talk about issues that occur when debugging locally but work fine in a production environment).

The issue I'm running into is that whenever an exception is thrown from my WCF service, the success handler of the $.ajax call is fired. The response is empty, the status text is "Success" and the response code is 202/Accepted.

The IErrorHandler implementation does get used because I can step through it and watch the FaultMessage get created. What happens in the end is that the success callback throws an error because the response text is empty when it is expecting a JSON string. The error callback never fires.

One thing that provided a little insight was removing the enableWebScript option from the endpoint behavior. Two things happened when I did this:

  1. The responses were no longer wrapped (i.e. no { d: "result" }, just "result").
  2. The error callback is fired, but the response is only the HTML for the 400/Bad Request yellow-screen-of-death from IIS, not my serialized fault.

I've tried as many things as show up in the top 10 results or more from Google regarding random combinations of the keywords "jquery ajax asp.net wcf faultcontract json", so if you plan on googling for an answer, don't bother. I'm hoping somebody on SO has run into this issue before.

Ultimately what I want to achieve is:

  1. Be able to throw any type of Exception in a WCF method
  2. Use a FaultContact
  3. Trap the exceptions in the ShipmentServiceErrorHandler
  4. Return a serialized ShipmentServiceFault (as JSON) to the client.
  5. Have the error callback invoked so I can handle item 4.

Possibly related to:

  • WCF IErrorHandler Extension not returning specified Fault

Update 1

I examined the output from tracing System.ServiceModel activity, and at one point after calling the UpdateCountry method, an exception is thrown, the message being

Server returned an invalid SOAP Fault.

and that's it. An inner exception complains about the serializer expecting a different root element, but I can't decipher much else out of it.


Update 2

So with some more messing around, I got something to work, though not the way I would consider ideal. Here's what I did:

  1. Removed the <enableWebScript /> option from the endpoint behavior section of the web.config.
  2. Removed the FaultContract attribute from the service method.
  3. Implemented a subclass of WebHttpBehavior (called ShipmentServiceWebHttpBehavior) and overrode the AddServerErrorHandlers function to add the ShipmentServiceErrorHandler.
  4. Changed the ShipmentServiceErrorHandlerElement to return an instance of type of ShipmentServiceWebHttpBehavior instead of the error handler itself.
  5. Moved the <errorHandler /> line from the service behavior section of the web.config to the endpoint behavior section.

It's not ideal because now WCF ignores the BodyStyle = WebMessageBodyStyle.WrappedRequest I want on my service methods (though I can now omit it altogether). I also had to change some of the code in the JS service proxy because it was looking for a wrapper ({ d: ... }) object on the responses.


Here is all of the relevant code (the ShipmentServiceFault object is pretty self explanatory).

The Service

My service is dead simple (truncated version):

[ServiceContract(Namespace = "http://removed")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class ShipmentService
{

    [OperationContract]
    [WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.WrappedRequest)]
    [FaultContract(typeof(ShipmentServiceFault))]
    public string UpdateCountry(Country country)
    {
        var checkName = (country.Name ?? string.Empty).Trim();
        if (string.IsNullOrEmpty(checkName))
            throw new ShipmentServiceException("Country name cannot be empty.");

        // Removed: try updating country in repository (works fine)

        return someHtml; // new country information HTML (works fine)
    }

}

Error Handling

The IErrorHandler, IServiceBehavior implementation is as follows:

public class ShipmentServiceErrorHandlerElement : BehaviorExtensionElement
{
    protected override object CreateBehavior()
    {
        return new ShipmentServiceErrorHandler();
    }

    public override Type BehaviorType
    {
        get
        {
            return typeof(ShipmentServiceErrorHandler);
        }
    }
}

public class ShipmentServiceErrorHandler : IErrorHandler, IServiceBehavior
{
    #region IErrorHandler Members

    public bool HandleError(Exception error)
    {
        // We'll handle the error, we don't need it to propagate.
        return true;
    }

    public void ProvideFault(Exception error, System.ServiceModel.Channels.MessageVersion version, ref System.ServiceModel.Channels.Message fault)
    {
        if (!(error is FaultException))
        {
            ShipmentServiceFault faultDetail = new ShipmentServiceFault
            {
                Reason = error.Message,
                FaultType = error.GetType().Name
            };

            fault = Message.CreateMessage(version, "", faultDetail, new DataContractJsonSerializer(faultDetail.GetType()));

            this.ApplyJsonSettings(ref fault);
            this.ApplyHttpResponseSettings(ref fault, System.Net.HttpStatusCode.InternalServerError, faultDetail.Reason);
        }
    }

    #endregion

    #region JSON Exception Handling

    protected virtual void ApplyJsonSettings(ref Message fault)
    {
        // Use JSON encoding  
        var jsonFormatting = new WebBodyFormatMessageProperty(WebContentFormat.Json);

        fault.Properties.Add(WebBodyFormatMessageProperty.Name, jsonFormatting);
    }

    protected virtual void ApplyHttpResponseSettings(ref Message fault, System.Net.HttpStatusCode statusCode, string statusDescription)
    {
        var httpResponse = new HttpResponseMessageProperty()
        {
            StatusCode = statusCode,
            StatusDescription = statusDescription
        };

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

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

    #endregion

    #region IServiceBehavior Members

    public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
    {
        // Do nothing
    }

    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
    {
        IErrorHandler errorHandler = new ShipmentServiceErrorHandler();

        foreach (ChannelDispatcherBase channelDispatcherBase in serviceHostBase.ChannelDispatchers)
        {
            ChannelDispatcher channelDispatcher = channelDispatcherBase as ChannelDispatcher;

            if (channelDispatcher != null)
            {
                channelDispatcher.ErrorHandlers.Add(errorHandler);
            }
        }
    }

    public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
    {
        // Do nothing
    }

    #endregion
}

The JavaScript

Calling the WCF method begins with:

    function SaveCountry() {
        var data = $('#uxCountryEdit :input').serializeBoundControls();
        ShipmentServiceProxy.invoke('UpdateCountry', { country: data }, function(html) {
            $('#uxCountryGridResponse').html(html);
        }, onPageError);
    }

The service proxy I mentioned earlier takes care of a lot of things, but at the core, we get to here:

$.ajax({
    url: url,
    data: json,
    type: "POST",
    processData: false,
    contentType: "application/json",
    timeout: 10000,
    dataType: "text",  // not "json" we'll parse
    success: function(response, textStatus, xhr) {

    },
    error: function(xhr, status) {                

    }
});

Configuration

I feel like the problems may lie here, but I've tried just about every combination of settings from everywhere I can find on the 'net that has an example.

<system.serviceModel>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
    <behaviors>
        <endpointBehaviors>
            <behavior name="Removed.ShipmentServiceAspNetAjaxBehavior">
                <webHttp />
                <enableWebScript />
            </behavior>
        </endpointBehaviors>
        <serviceBehaviors>
            <behavior name="Removed.ShipmentServiceServiceBehavior">
                <serviceMetadata httpGetEnabled="true"/>
                <serviceDebug includeExceptionDetailInFaults="false"/>
                <errorHandler />
            </behavior>
        </serviceBehaviors>
    </behaviors>
    <services>
        <service name="ShipmentService" behaviorConfiguration="Removed.ShipmentServiceServiceBehavior">
            <endpoint address="" 
                behaviorConfiguration="Removed.ShipmentServiceAspNetAjaxBehavior" 
                binding="webHttpBinding" 
                contract="ShipmentService" />
        </service>
    </services>
    <extensions>
        <behaviorExtensions>
            <add name="errorHandler" type="Removed.Services.ShipmentServiceErrorHandlerElement, Removed, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
        </behaviorExtensions>
    </extensions>
</system.serviceModel>

Notes

I noticed this questions is getting a few favorites. I did find the solution to this issue and I hope to provide an answer when I find some time. Stay tuned!

like image 561
Cᴏʀʏ Avatar asked Dec 05 '10 22:12

Cᴏʀʏ


3 Answers

I'm not familiar with ASP or WCF, but I am quite familiar with jQuery. The one thing that sticks out in my mind about your question is that your service is returning 202 Success when an exception is thrown. jQuery chooses which callback to call (success or error) based on the HTTP status code that is returned from the server. 202 is considered a successful response, and therefor jQuery will call success. If you want to have jQuery call the error callback, you need to make your service return a 40x or 50x status code. Consult http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for a list of HTTP status codes.

like image 184
David Avatar answered Oct 19 '22 19:10

David


I had the same symptoms with a different scenario so this may or may not help.

Here is a breif summary of what I was doing and our solution:

I was posting to a REST implementation of a WCF service that we host from a classic ASP page. I found I had to set the input as a stream and read from that, disposing of the stream when done. I beleive it was at this point I was getting the 202 response with text of "success" as you have described. I discovered that by not disposing of the stream I was getting the response I was expecting for error conditions.

Here is a summary of the final code:

[WebHelp(Comment="Expects the following parameters in the post data:title ...etc")] 
    public int SaveBook(Stream stream)
    {
        NameValueCollection qString;
        StreamReader sr = null;
        string s;
        try
        {
            /**************************************************************************************
             * DO NOT CALL DISPOSE ON THE STREAMREADER OR STREAM                                  *
             * THIS WILL CAUSE THE ERROR HANDLER TO RETURN A PAGE STATUS OF 202 WITH NO CONTENT   *
             * IF THERE IS AN ERROR                                                               *

             * ***********************************************************************************/
            sr = new StreamReader(stream);
            s = sr.ReadToEnd();
            qString = HttpUtility.ParseQueryString(s);

            string title = qString["title"];

            //Do what we need

            //Then Return something
            int retRecieptNum = UtilitiesController.SubmitClientEntryRequest(entryReq);                

            return retRecieptNum;
        }
        catch (Exception ex)
        {
            throw new WebProtocolException(System.Net.HttpStatusCode.Forbidden, ex.Message, this.GetExceptionElement(true, "BookRequest", ex.Message), false, ex);
        }
        finally
        {

        }            
    }

Hopefully this is some help to you, maybe try using a stream and see how that goes.

like image 42
Jon P Avatar answered Oct 19 '22 19:10

Jon P


Have you looked at JSON.NET? I was using it to convert objects in c# to JSON friendly strings then passing it back across the wire to my client where I parsed it into a JSON object. In the end I got rid of it and went to JSON2 for stringify. Here is my ajax call I use:

function callScriptMethod(url, jsonObject, callback, async) {

    callback = callback || function () { };
    async = (async == null || async);

    $.ajax({
        type: 'POST',
        contentType: 'application/json; charset=utf-8',
        url: url,
        data: JSON.stringify(jsonObject),
        dataType: 'json',
        async: async,
        success: function (jsonResult) {
            if ('d' in jsonResult)
                callback(jsonResult.d);
            else
                callback(jsonResult);
        },
        error: function () {
            alert("Error calling '" + url + "' " + JSON.stringify(jsonObject));
            callback([]);
        }
    });
}
like image 1
colemande Avatar answered Oct 19 '22 19:10

colemande