The problem:
You cannot call remote ASP.NET Web Service methods from a JavaScript, AJAX client.
Example:
You have a Web Service at this address: http://a.com/service.asmx and you've configured the service to work with AJAX clients:
[WebService
(Namespace = "http://www.hyzonia.com/gametypes/PopNDropLikeGame/WS2")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService]
public class GameService : System.Web.Services.WebService
{
[WebMethod(EnableSession = true)]
public GameSessionResponse CreateGameSession(Guid questId)
{
...
}
}
And it works fine when you call its methods from a web page that is in this address: http://a.com/page.htm:
$.ajax({
type: "POST",
url: "GameService.asmx/CreateGameSession",
data: "{questId: '" + questId + "'}",
cache: false,
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(response) {
Game._onSessionGot(response.d);
}
});
But the very same client-side code doesn’t work from this address: http://b.clom/page.htm.
The problem in depth:
At first, it is a silly problem, for me it is an overprotection. After all, Web Services are meant to be called by remote clients. The fact that browsers block access to Web Services by AJAX calls is clearly contrary to the purpose of Web Services.
Interestingly, browser extensions like Flash and Silverlight also, by default, block remote Web Services, but they provide a workaround. Unfortunately, no browser by date supports this work around for XMLHttpRequest
s. This "security measure" seems odder when we notice that it is perfectly correct to import a JavaScript code snippet from another domain using a script tag:
<script
src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"
type="text/javascript">
</script>
The solution:
As it was said, Flash and Silverlight both support remote calls. You just need a client access policy file to be hosted at the root of a.com (http://a.com/clientaccesspolicy.xml):
="1.0"="utf-8"
<access-policy>
<cross-domain-access>
<policy>
<allow-from http-request-headers="SOAPAction">
<domain uri="*"/>
</allow-from>
<grant-to>
<resource path="/" include-subpaths="true"/>
</grant-to>
</policy>
</cross-domain-access>
</access-policy>
This file allows remote calls to be made from any other domain.
But in many situations, we want to call the Web Service methods directly from AJAX clients. This need was the cause of the development of JSONP (JSON with padding) protocol. As it was discussed, it is correct to have a <script>
element that loads a script from another domain. On the other hand, you may know that it is possible to load scripts dynamically by a simple JavaScript trick (writing <script>
tags) or using this jQuery plug in. Now the bulbs are flickering! The solution is to access the JSON Web Service by the src
attribute of a <script>
element. This is the whole idea behind JSONP.
But there are a couple of problems needed to be solved for ASP.NET ASMX Web Services before we can use them in a JSONP scenario.
- ASP.NET Web Services by default only accept POST requests; a
<script src="">
element, produces a GET request. - The result of the web method call must conform to JSONP, and as you can guess, ASP.NET 3.5 by default doesn’t support it.
The solution to the first problem may seem trivial, we can easily enable GET calls to web methods using the [ScriptMethod(UseHttpGet = true)]
attribute. The immediate problem is that when we mark a web method by this attribute, it can only be called by GET requests. And remember, other clients (actually anything other than JSONP clients) are supposed to communicate with the web service by POST requests. I usually end up inheriting from the original Web Service and marking web methods by the [ScriptMethod(UseHttpGet = true)]
attribute in the derived class. Therefore, I will have two ASMX Web Services, one using the original class (expecting POST requests) and the other using the derived class (expecting GET requests).
[WebMethod(), ScriptMethod(UseHttpGet = true)]
public override GameSessionResponse CreateGameSession(Guid questId)
{
return base.CreateGameSession(questId);
}
Note you may need to add this code snippet in web.config:
<system.web>
<webServices>
<protocols>
<add name="HttpGet"/>
</protocols>
</webServices>
…
</system.web>
There's another problem to be addressed in the client side. The client should call the web method using the correct URL (it has to pass the correct query string that could be deserialized back to .NET objects in the server side). In case of POST requests, I'm used to JSON2 library to post data to ASP.NET ASMX Web Services. JQuery $.AJAX
method (when it is configured to use JSONP, using dataType: "jsonp"
) creates query string parameters for the data objects it receives. But the result is not usable for ASMX Web Services.
Luckily, there's a ready to use JQuery plug-in (jMsAjax) that has the required algorithms for serializing a JavaScript object into a query string that can be parsed by ASP.NET Web Services.
Using the plug-in, I created this function to serialize JavaScript objects into query strings:
$.jmsajaxurl = function(options) {
var url = options.url;
url += "/" + options.method;
if (options.data) {
var data = ""; for (var i in options.data) {
if (data != "")
data += "&"; data += i + "=" +
msJSON.stringify(options.data[i]);
}
url += "?" + data; data = null; options.data = "{}";
}
return url;
};
You will need jMsAjax for this code snippet to work.
Finally, this is a sample of a client side code using JQuery that calls an ASMX Web Service using JSONP:
var url = $.jmsajaxurl({
url: "http://hiddenobjects.hyzonia.com/services/GameService3.asmx",
method: "Login",
data: { email: "myemail@mydomain.com", password: "mypassword" }
});
$.ajax({
cache: false,
dataType: "jsonp",
success: function(d) { console.log(d); },
url: url + "&format=json"
});
Or equivalently:
$.getJSON(url + "&callback=?&format=json", function(data) {
console.log(data);
});
When you call an ASP.NET Web Service method (that is configured to receive GET requests) using a code similar to the above, it returns in XML. The problem is that the Web Service expects to receive a request that has a content type of "application/json; charset=utf-8"
and the <script>
element simply doesn't add this content type to the request. There's a little thing we can do at the client side. The easiest way to resolve this problem is to use an HTTP module. The HTTP module should add this content type to the requests before they are processed by the Web Service handler.
On the other hand, a JSONP client expects that the Web Service returns the call by a string like this:
nameOfACallBackFunction(JSON_OBJECT_WEB_METHOD_RETURNED)
nameOfACallBackFunction
must be given to the server by a parameter in the query string. Different JSONP compatible Web Services use different names for this parameter, but usually it is named 'callback'. At least, this is what $.ajax()
automatically adds to the request in JSONP mode.
We have to modify the response stream that the server is returning. Luckily, in ASP.NET, it is easy to apply a filter to the response.
I slightly modified this HTTP module that I originally grabbed from a post in elegantcode.com, to improve its performance:
public class JsonHttpModule : IHttpModule
{
private const string JSON_CONTENT_TYPE =
"application/json; charset=utf-8";
public void Dispose()
{
}
public void Init(HttpApplication app)
{
app.BeginRequest += OnBeginRequest;
app.ReleaseRequestState += OnReleaseRequestState;
}
bool _Apply(HttpRequest request)
{
if (!request.Url.AbsolutePath.Contains(".asmx")) return false;
if ("json" != request.QueryString.Get("format")) return false;
return true;
}
public void OnBeginRequest(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
if (!_Apply(app.Context.Request)) return;
if (string.IsNullOrEmpty(app.Context.Request.ContentType))
{
app.Context.Request.ContentType = JSON_CONTENT_TYPE;
}
}
public void OnReleaseRequestState(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
if (!_Apply(app.Context.Request)) return;
app.Context.Response.Filter =
new JsonResponseFilter(app.Context.Response.Filter, app.Context);
}
}
public class JsonResponseFilter : Stream
{
private readonly Stream _responseStream;
private HttpContext _context;
public JsonResponseFilter(Stream responseStream, HttpContext context)
{
_responseStream = responseStream;
_context = context;
}
public override void Write(byte[] buffer, int offset, int count)
{
var b1 = Encoding.UTF8.GetBytes(
_context.Request.Params["callback"] + "(");
_responseStream.Write(b1, 0, b1.Length);
_responseStream.Write(buffer, offset, count);
var b2 = Encoding.UTF8.GetBytes(");");
_responseStream.Write(b2, 0, b2.Length);
}
}
This HTTP module will be applied to each request to an .asmx file that has a format=json
in its query string. Note that you have to update web.config:
<system.web>
…
<httpModules>
…
<add name="JSONAsmx" type="JsonHttpModule, App_Code"/>
</httpModules>
</system.web>
for IIS6, and:
<system.webServer>
<modules>
…
<add name="JSONAsmx" type="JsonHttpModule, App_Code"/>
</modules>
…
</system.webServer>
for IIS7.
Now to test it, let's open the Web Service in a browser window; in my example, http://hiddenobjects.hyzonia.com/services/GameService3.asmx/Login?email=e@e.com&password=p should return in XML, and http://hiddenobjects.hyzonia.com/services/GameService3.asmx/Login?email="e@e.com"&password="p"&format=json&callback=myCallBackFunc will return:
myCallBackFunc({"d":{"__type":"HLoginResponse",
"isSuccessful":false,"error":false,"authSessionId":null,
"nickName":null,"score":0}});
Don't worry about myCallBackFunc
, JQuery nicely manages it so that the whole business is behind the scene and you can use the $.ajax
success callback the very same way you use it for a normal AJAX call.
We should note that JSONP has its own problems, especially… yes... in IE! All versions of Internet Explorer have a 2083 character limit for the URL of a request. It means that you cannot send large data in GET requests to the server. Sometimes this limitation leaves us with no choice but to use Flash or create a proxy to the remote Web Service in the local domain.