Homam's Mind

Monday, October 12, 2009

Accessing Remote ASP.NET Web Services using JSONP

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 I cannot hold myself and not say that it is a silly problem. 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 work around. Unfortunately no browser by date supports this work around for XMLHTTPRequests. This "security measure" seems odder when we notice that it is perfectly correct to import a JavaScript code snippet from another domain by 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 clientaccesspolicy file to be hosted at the root of a.com (http://a.com/clientaccesspolicy.xml):
<?xml version="1.0" encoding="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 by AJAX clients. This need was the cause of to 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.

  1. ASP.NET web services by default only accept POST requests, a <script src=""> element, produces a GET request.

  2. The result of the web method call must conform to JSONP, and you 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 [ScriptMethod(UseHttpGet = true)] attribute. The immediate problem is that when we mark a web method by this attribute it only can 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 [ScriptMethod(UseHttpGet = true)] attribute in the derived class. Therefore I will have two ASMX web services, one using the original class (expecting POST request) 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 by a correct URL (it has to pass a 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, by dataType: "jsonp") creates query string parameters for the data object 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 <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 a 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 return 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.

I grabbed this HTTP module from a post in elegantcode.com.
public class JsonHttpModule : IHttpModule
{
private const string JSON_CONTENT_TYPE = "application/json; charset=utf-8";

#region IHttpModule Members
public void Dispose()
{
}

public void Init(HttpApplication app)
{
app.BeginRequest += OnBeginRequest;
app.ReleaseRequestState += OnReleaseRequestState;
}
#endregion

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 bool CanRead { get { return true; } }

public override bool CanSeek { get { return true; } }

public override bool CanWrite { get { return true; } }

public override long Length { get { return 0; } }

public override long Position { get; set; }

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);
}

public override void Close()
{
_responseStream.Close();
}

public override void Flush()
{
_responseStream.Flush();
}

public override long Seek(long offset, SeekOrigin origin)
{
return _responseStream.Seek(offset, origin);
}

public override void SetLength(long length)
{
_responseStream.SetLength(length);
}

public override int Read(byte[] buffer, int offset, int count)
{
return _responseStream.Read(buffer, offset, count);
}
}

This HTTP module will be applied to each request to a .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"/>
</httpModules>
</system.web>

For IIS6 and
<system.webServer>
<modules>

<add name="JSONAsmx"/>
</modules>

</system.webServer>

For IIS7.

Now to test is open your web service in your browser, in my example:

http://hiddenobjects.hyzonia.com/services/GameService3.asmx/Login?email=e@e.com&password=p

It 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 $.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 has a 2083 character limit for the URL of a request. It means that you cannot send large data in GET requests to the server. Sometime this limitation leaves us with no choice but to use Flash or create a proxy to the remote web service in the local domain.

3 comments:

Pawel said...

Hi,

First of all, thanks for this useful document, it has been perfect for me. The example and the code work fine when I'm using Firefox and with IE when I'm using a local server but when I try to use a remote server with the IE I always get the same error with the debug in the jquery 1.4.2:

Code:
// If we're requesting a remote document
// and trying to load JSON or Script with a GET
if ( s.dataType === "script" && type === "GET" && remote ) {
var head = document.getElementsByTagName("head")[0] || document.documentElement;
var script = document.createElement("script");
script.src = s.url; if ( s.scriptCharset ) {
script.charset = s.scriptCharset;
}

Error: Operation invalid.

That's all the information that I have and now I want buy a gun or something and destroy microsoft ! hahaha. Please, if anybody knows how to to this, tell what you got it.

Regads,
Pawel.

Theopilus said...

im implement this in autocomplete but cannot resolve , please help me

this is my thread :

http://forums.asp.net/p/1754328/4759449.aspx/1?Re+Autocomplete+Access+data+by+WebService+in+Server+

salman said...

Nice and very much helpful... worked for me .. Thanks :)