3 Dec 2011

3 mistakes to avoid when using jQuery with ASP.NET AJAX


On the bright side, your great feedback in both posts’ comments has reinforced the fact that some of the best content on my blog is the part that you write.
In this post, I’m going to detail three of the problems that were discovered as a result of those previous two posts:
  • An extra requirement when making a read-only request to IIS6+.
  • An oddity in Internet Explorer 7′s XmlHttpRequest class.
  • A common mistake when passing JSON parameters through jQuery.
Finally, I’ll suggest what I now believe to be a best practice usage, taking all of these issues into account.


Security requirements when POSTing to IIS

This error message was the most common problem that was reported:
Length Required
The underlying issue is that most installations of IIS6+ require a content-length be provided with all POST requests, even if there is no content (POST data).
The content-length for a request with no data should be 0, but jQuery doesn’t set that header automatically unless there is a data parameter. Since ASP.NET AJAX’s JSON serialized services require a POST request, this becomes a stumbling block for read-only requests.
The easiest way I’ve found to work around this is to use an empty JSON object as a parameter on read-only requests. For example, if you were calling a page method:
$.ajax({
  type: "POST",
  url: "PageMethod.aspx/PageMethodName",
  data: "{}",
  contentType: "application/json; charset=utf-8",
  dataType: "json"
});
This will cause jQuery to correctly set a content-length of 2, while your web service or page method will happily ignore the empty parameter and treat the request as read-only.
You might notice the content-type is set differently in this example than in my previous posts. More on that next:

A problem with Internet Explorer and XmlHttpRequest

Previously, to call ASP.NET AJAX services with jQuery, I suggested this usage:
$.ajax({
  type: "POST",
  url: "WebService.asmx/WebMethodName",
  beforeSend: function(xhr) {
    xhr.setRequestHeader("Content-type", 
                         "application/json; charset=utf-8");
  },
  dataType: "json"
});
The reason for the beforeSend delegate is due a quirk in jQuery which causes it to send a specified content-type only if there is also a data parameter specified.
For security reasons, ASP.NET AJAX will not provide a JSON serialized response unless the proper content-type is provided. By using the beforeSend delegate as shown, the content-type will be manually set on the XmlHttpRequest object, before the request.
However, the new XmlHttpRequest class in Internet Explorer 7 doesn’t implement setRequestHeader very intuitively. Instead of setting the specified header, it appends the value.
This becomes a problem when you make a request that includes a data parameter. When POST data is provided, jQuery will automatically set the content-type to its default of application/x-www-form-urlencoded. In the beforeSend delegate, the required application/json; charset=utf-8 will be appended after jQuery’s default.
End result? Because this amalgamation of content-types isn’t what ASP.NET AJAX expects, the web service will not return JSON serialized content and jQuery will be unable to parse it.
Solution? If you’re sending data in your request, use jQuery’s contentType parameter to set the content-type, so that the default is never added:
$.ajax({
  type: "POST",
  url: "WebService.asmx/WebMethodName",
  data: "{'fname':'dave', 'lname':'ward'}",
  contentType: "application/json; charset=utf-8",
  dataType: "json"
});

JSON, objects, and strings: oh my!

ASP.NET AJAX script services and page methods understand and expect input parameters to be serialized as JSON strings. These parameters are parsed out of the POST data and used as arguments to the method you’ve called.
However, if you directly provide a JSON object as the data parameter for an $.ajax call, jQuery will attempt to URL encode the object instead of passing it on to your web service.
Take this sample request, for example:
$.ajax({
  type: "POST",
  url: "WebService.asmx/WebMethodName",
  data: {'fname':'dave', 'lname':'ward'},
  contentType: "application/json; charset=utf-8",
  dataType: "json"
});
Because that data parameter is a valid object literal, calling the web service this way won’t throw any JavaScript errors on the client-side. Unfortunately, it won’t have the desired result either. Instead of passing that object through to the web service as JSON, jQuery will automatically URL encode it and send it as:
fname=dave&lname=ward
To which, the server will respond with:
Invalid JSON primitive: fname.
This is clearly not what we want to happen. The solution is to make sure that you’re passing jQuery a string for the data parameter, like this:
$.ajax({
  type: "POST",
  url: "WebService.asmx/WebMethodName",
  data: "{'fname':'dave', 'lname':'ward'}",
  contentType: "application/json; charset=utf-8",
  dataType: "json"
});
It’s a small change in syntax, but makes all the difference. Now, jQuery will leave our JSON object alone and pass the entire string to ASP.NET for parsing on the server side.

Best practice? I hope so this time!

Taking these three caveats into account, I think this is the best practice for calling read-only ASP.NET AJAX web services via jQuery:
$.ajax({
  type: "POST",
  url: "ServiceName.asmx/WebMethodName",
  data: "{}",
  contentType: "application/json; charset=utf-8",
  dataType: "json",
  success: function(msg) {
    // Do interesting things here.
  }
});
When consuming web services that require parameters, the usage is similar. Just make sure that you’re passing a JSON object in a string:
$.ajax({
  type: "POST",
  url: "ServiceName.asmx/WebMethodName",
  data: "{'fname':'dave','lname':'ward'}",
  contentType: "application/json; charset=utf-8",
  dataType: "json",
  success: function(msg) {
    // Do magic here.
  }
});
In either of these cases, substitute PageName.aspx for ServiceName.asmx and PageMethodName for WebMethodName if you want to call a page method instead of a web service.

No comments:

Post a Comment