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
andIServiceBehavior
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:
- The responses were no longer wrapped (i.e. no
{ d: "result" }
, just"result"
). - 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:
- Be able to throw any type of
Exception
in a WCF method - Use a
FaultContact
- Trap the exceptions in the
ShipmentServiceErrorHandler
- Return a serialized
ShipmentServiceFault
(as JSON) to the client. - 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:
- Removed the
<enableWebScript />
option from the endpoint behavior section of the web.config. - Removed the
FaultContract
attribute from the service method. - Implemented a subclass of
WebHttpBehavior
(calledShipmentServiceWebHttpBehavior
) and overrode theAddServerErrorHandlers
function to add theShipmentServiceErrorHandler
. - Changed the
ShipmentServiceErrorHandlerElement
to return an instance of type ofShipmentServiceWebHttpBehavior
instead of the error handler itself. - 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!
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.
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.
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([]);
}
});
}
Here is another shot. I'll leave my original attempt incase that solution helps someone else.
To fire the error condidition for the $.ajax call you will need an error code in your response
protected virtual void ApplyHttpResponseSettings(ref Message fault, System.Net.HttpStatusCode statusCode, string statusDescription)
{
var httpResponse = new HttpResponseMessageProperty()
{
//I Think this could be your problem, if this is not an error code
//The error condition will not fire
//StatusCode = statusCode,
//StatusDescription = statusDescription
//Try forcing an error code
StatusCode = System.Net.HttpStatusCode.InternalServerError;
};
httpResponse.Headers[HttpResponseHeader.ContentType] = "application/json";
httpResponse.Headers["jsonerror"] = "true";
fault.Properties.Add(HttpResponseMessageProperty.Name, httpResponse);
}
Heres hoping my second attmpt is more useful to you!
I had a similar problem sort of with WCF and using ASP.NET compatibility as I integrate MVC and WCF in my solution. What I would do is throw a WebFaultException then check the Status of the response at the receiving end (either java or other .NET client). Your custom error could then throw that if the WebOperationContext.Current is not null. You are probably aware of this already but just thought I would throw it out there.
throw new WebFaultException(HttpStatusCode.BadRequest);
精彩评论