Adventures with JSONP and jQuery

This whole thing started out as a nice-to-have. I have a blog (you’re reading it). I have a URL shortener (jmbk.nl). They are separate apps on separate domains. When I publish a post here, I diligently create the short URL for it manually in order to publish that short URL on social sites (the URL shortener has some minimalist stats associated with each short URL; so minimal, it’s only a count of the number of times it was used). Yeah, I know, silly, huh: why can’t each post generate its own short URL?

Compass on old mapNow I could have done this in C# and .NET as a plug-in to the blogging software I use, but where’s the fun in that? Let’s do it in JavaScript!

What I want is some code that will get the URL of a blog post when that post is displayed in the browser (easy!), call my URL shortener service with it via good-old AJAX, and receive the short URL as a reply. The big issue about this simple plan is the so-called “Same-Origin Policy”. In essence, getting data from the same site (that is, protocol, domain/host, and port number) via JavaScript is smiled upon, but getting data from another site entirely is frowned upon to the point it doesn’t work. Since my pages are on blog.boyet.com, I can only get data from blog.boyet.com. It’s all to do with maintaining strict security boundaries (think of cookies as a big example).

Nevertheless, sometimes it might be advantageous for your code to get some data from another site. An example might be to display the top ten most recent of your tweets from Twitter in your blog webpage. The problem here is that twitter.com is not the domain of your personal blog, so what can be done?

If you think about it, the one thing you can get from other sites is JavaScript code inside script elements. Here as an example is how this site gets jQuery:

<script type="text/javascript" src="http://ajax.microsoft.com/ajax/jQuery/jquery-1.9.1.min.js"></script>

When the browser encounters a script element likes this, two things happen: first, the JavaScript file referenced by that URL is downloaded (via HTTP GET), and second, the script thus downloaded is executed. Hmm.

Let’s continue this thought experiment. These days we get our data in the JSON format; that is, code that defines a JavaScript object. Here’s an example of a JSON object that is relevant to my discussion:

{
    "shortURL": "http://jmbk.nl/j6Q2K",
    "requestCount": 42
}

Running before I can walk, I could construct the source URL I need in a <script> tag, say something like “http://jmbk.nl/MakeUrl/?shorten=http://blog.boyet.com/” (that isn’t the real URL I use for this operation, by the way, so there’s no point in trying it), and the browser would GET the URL, which would return the above JSON. Which would then be executed and crash with some kind of syntax error: it’s not executable code.

OK, then, let’s alter the URL shortener’s code to return this instead:

foobar({ "shortURL": "http://jmbk.nl/j6Q2K", "requestCount": 42 });

This time it’s real executable code, and the browser will call the foobar() function for me once the AJAX request is done, passing the JSON object I want as a parameter. Except I don’t have a foobar() function yet in my client code, so I have to write one and include its code in some kind of script tag. This function must take the JSON object and so something with it (like replace the href attribute on an anchor element I have somewhere in my HTML markup).

This, in essence, is this mysterious protocol called JSONP you may have heard of. JSONP stands for JSON with Padding, where the “padding” is wrapping the JSON object inside a function call as a parameter.

Now it’s a real pain to have to have two different ways of getting JSON objects from servers: one for your “same origin” server (a simple AJAX get request) and one for everything else (adding a silly script element, having a special function that does something with the JSON object, yadda, yadda), so jQuery simplifies it in one function: $.getJSON(). The code in this function looks at the origin for the URL that’s passed in. If it’s for the same site, the function issues an AJAX get request and the callback gets called normally on completion. In the other case, jQuery does some pretty fancy footwork in the background that creates a temporary script element on the fly with a slightly modified version of the URL you pass in, creates a callback function that saves the returned JSON object and that calls your callback with that object (and deletes the temporary script element). To you, the developer, it’s as if a ‘local’ and a ‘remote’ getJSON call just work in the same way: you specify the URL, and your callback is executed once the JSON object is returned.

I mentioned that for a ‘remote’ call the URL is modified a little: jQuery adds a callback query string to the end of the URL. This query string defines the name of the function that should wrap the returned JSON. For example, for “http://jmbk.nl/MakeUrl/?shorten=someurl”, the URL actually used is something like “http://jmbk.nl/MakeUrl/?shorten=someurl&callback=foobar”. The server is supposed to read this query string and construct the reply so that it becomes a call to this function. Better still is to be explicit about the callback function name: write the URL as “http://jmbk.nl/MakeUrl/?shorten=someurl&callback=?” and jQuery will make up a function with a unique name on the fly. This is by far the preferred way to do it: jQuery will ensure the returned JSON is not cached for example.

(Note: it does get a little confusing since there are two callbacks in play. There’s the callback function you write and pass to the $.getJSON() function. This function has one parameter: the JSON object that is returned. This callback will get called by jQuery once the AJAX request has completed. The other callback is the function that is used by the server to return the JSON. The browser calls this callback by executing the code returned from the AJAX request.)

Of course, in my case, since the URL I’m trying to shorten may itself have query strings, it behooves me to encode the long URL and for the server to decode it. Here’s the code I use on the client:

$(function () {
    var url = encodeURIComponent(http://blog.boyet.com/?foo=this&bar=that);
    url = "http://jmbk.nl/MakeUrl/?shorten=" + url + "&callback=?";
    $.getJSON(url, function (json) {
        $("#shortURL").attr("href", json.shortUrl);
    });
});

And here’s the (somewhat redacted) code on the server:

  public void ProcessRequest(HttpContext context) {
    string urlToShorten = context.Request.QueryString["shorten"];
    if (!string.IsNullOrEmpty(urlToShorten)) {
      string jsonpCallback = context.Request.QueryString["callback"];
      if (!string.IsNullOrEmpty(jsonpCallback)) {
        context.Response.ContentType = "text/javascript";
        string responseFormat = jsonpCallback + "({{ \"shortUrl\" : '{0}', \"requestCount\" : {1} }})";
        urlToShorten = Uri.UnescapeDataString(urlToShorten);   

        ShortUrl shortUrl = new ShortUrl(urlToShorten);
        shortUrl.Save();

        context.Response.Write(string.Format(responseFormat, shortUrl.PublicShortenedUrl, shortUrl.UsageCount));
      }
    }
  }

And that’s about it. If you want to do more with JSONP, check out the relevant options for $.ajax.

Album cover for The Power Of SuggestionNow playing:
Karminsky Experience Inc. - Exploration
(from The Power Of Suggestion)


Loading similar posts...   Loading links to posts on similar topics...

No Responses

Feel free to add a comment...

Leave a response

Note: some MarkDown is allowed, but HTML is not. Expand to show what's available.

  •  Emphasize with italics: surround word with underscores _emphasis_
  •  Emphasize strongly: surround word with double-asterisks **strong**
  •  Link: surround text with square brackets, url with parentheses [text](url)
  •  Inline code: surround text with backticks `IEnumerable`
  •  Unordered list: start each line with an asterisk, space * an item
  •  Ordered list: start each line with a digit, period, space 1. an item
  •  Insert code block: start each line with four spaces
  •  Insert blockquote: start each line with right-angle-bracket, space > Now is the time...
Preview of response