Making JSON-P web API calls more robust

ORIGINALLY POSTED JUNE 17, 2009

Recently I had a discussion with some online colleagues about JSON-P web services.  There was a post on Ajaxian about how Microsoft Bing wraps their JSON-P results in a try..catch block.  I thought this was a neat little addition to JSON-P, which I’ve felt for a long time is an extremely interesting little hack.

Now, the point I made was that while the try…catch thing was a nice addition, it, of course, didn’t address what I see as the biggest problem with JSON-P, and that’s having no way to really know if the request completed or not.  We tossed around some ideas and a couple of us pretty quickly and independently come up with the idea of having a concurrent timeout with the in-flight request.

Before I go any further, I’ll qualify this all by saying I strongly suspect this isn’t an original idea, but I do know that, at least in my case (I can’t say what goes on in the head of others, I can barely make sense of what goes on in mind!) that I thought it up on my own.

Be that all as it may, the point is that it’s a viable approach, but it needed some refinement.  So, I threw together some code today.  First, a JavaScript file:

/**
 * JSONP makes JSON-P requests and do so with a timeout mechanism so you'll
 * always know whether the request was successful or not.
 */
var JSONP = {

  /* Amount of time in seconds before a request is considered timed out. */
  timeoutSeconds : 5,

  /* Reference to the document's head tag. */
  headTag : document.getElementsByTagName("head").item(0),

  /* Collection of objects, one for each in-flight request. */
  requestObjects : { },

  /**
   * Call this method to fire off a JSON-P request.
   *
   * @param inConfig An object containing whatever parameters are needed to
   * make the remote call.  The attributes of this object are used to construct
   * a query string.
   */
  request : function(inConfig) {

    // Create unique ID for request.
    var requestID = "req_" + new Date().getTime();

    // Create request object and populate with internal data.
    var requestObject = { };
    requestObject.requestID = requestID;
    requestObject.callback = function(inResponse) {
      JSONP.callback(requestID, inResponse);
    };
    requestObject.realCallback = inConfig.callback;
    requestObject.onTimeout = inConfig.onTimeout;
    inConfig.callback = "JSONP.requestObjects." + requestID + ".callback";

    // Add query string to URL.
    if (inConfig.url.charAt(inConfig.url.length - 1) != "?") {
      inConfig.url += "?";
    }
    var queryString = "";
    for (var attribute in inConfig) {
      if (queryString != "") {
        queryString += "&";
      }
      queryString += attribute + "=" + inConfig[attribute];
    }
    inConfig.url += queryString;
    requestObject.inConfig = inConfig;

    // Now create the script tag for this request.
    var scriptTag = document.createElement("script");
    requestObject.scriptTag = scriptTag;
    scriptTag.setAttribute("src", inConfig.url);
    scriptTag.setAttribute("type", "text/javascript");

    // Now create the timeout.
    requestObject.timeout = setTimeout(function() {
      JSONP.timeoutElapsed(requestID);
    }, this.timeoutSeconds * 1000);

    // Kick off the request.
    this.headTag.appendChild(scriptTag);

    // Finally, put the requestObject in the collection.
    this.requestObjects[requestID] = requestObject;

  },

  /**
   * Internal intermediary callback that the JSON-P request calls.
   *
   * @param inRequestID The ID associated with the requestObject.
   * @param inResponse  The response from the remote server.
   */
  callback : function(inRequestID, inResponse) {

    // Get the request object associated with this request.
    var requestObject = JSONP.requestObjects[inRequestID];

    // Might not have a reqested object, if the request comes back after the
    // timeout period.
    if (requestObject) {
      // Clear the timeout so the request doesn't time out.
      clearTimeout(requestObject.timeout);
      // Call the specified callback.
      requestObject.realCallback(inResponse);
      // Delete the request object.
      delete JSONP.requestObjects[inRequestID];
    }

  },

  /**
   * This is called when a request timeout occurs.
   *
   * @param inRequestID The ID associated with the requestObject.
   */
  timeoutElapsed : function(inRequestID) {

    // Get the request object associated with this request.
    var requestObject = JSONP.requestObjects[inRequestID];

    // There should never be a case where there is no requestObject here,
    // but we'll check for it anyway, just in case I missed something.
    if (requestObject) {
      // Copy pertinent attributes of requestObject to a new object.
      var newRequestObject = { };
      newRequestObject.requestID = requestObject.requestID;
      newRequestObject.inConfig = { };
      for (var i in requestObject.inConfig) {
        newRequestObject.inConfig[i] = requestObject.inConfig[i];
      }
      // Delete the request object, but get onTimeout first.
      var onTimeout = requestObject.onTimeout;
      delete JSONP.requestObjects[inRequestID];
      // Now call the real timeout handler, if any.
      if (onTimeout) {
        onTimeout(newRequestObject);
      }
    }

  },

}; // End JSONP.

Now, a test HTML file to use this:

<body>

  <head>

    <script src="jsonp.js"></script>
    <script>

      function test1() {
        var config = {
          url : "http://search.yahooapis.com/ImageSearchService/V1/imageSearch?",
          appid : "YahooDemo", query : "Amanda Tapping", output : "json",
          callback : test1Callback, onTimeout : timeoutHandler
        };
        JSONP.request(config);
      }

      function badTest() {
        var config = {
          url : "gibberish", appid : "gibberish", query : "gibberish",
          output : "json", callback : null, onTimeout : timeoutHandler
        };
        JSONP.request(config);
      }

      function timeoutHandler(inRequestObject) {
        // Iterate its attributes
        var s = "";
        for (var i in inRequestObject) {
          s += i + " = " + inRequestObject[i] + "\n";
        }
        for (var i in inRequestObject.inConfig) {
          s += i + " = " + inRequestObject.inConfig[i] + "\n";
        }
        // Show the output.
        alert("REQUEST TIMED OUT - Request Object Dump: \n\n" + s);
      }

      function test1Callback(inResponse) {
        var outputString = "Total results returned: " +
          inResponse.ResultSet.totalResultsReturned + "<br>";
        for (var i = 0; i < inResponse.ResultSet.Result.length; i++) {
          outputString += inResponse.ResultSet.Result[i].Title + "<br>";
        }
        document.getElementById("divResponse").innerHTML = outputString;
      }

    </script>

  </head>

  <body>

    <input type="button" value="Make Request" onClick="test1();">
    <br><br>
    <div id="divResponse"
      style="width:400px;height:250px;border:1px solid #000000;overflow:auto;">
      Search results will appear here</div>
    <br><br>
    <input type="button" value="Make Bad Request" onClick="badTest();">

  </body>

</html>

To use it, you simply call the JSONP.request() method.  You pass to this method an object that configures the call.  Only three of the attributes, strictly-speaking, are required.

  • url – The URL of the JSON-P web service to call.  This can end with a question mark, but it doesn’t have to.
  • callback – This is the function that will be called when the response comes back.
  • onTimeout – This is the function that will be called if the request times out.

The others are completely dependant on the JSON-P services you’re calling.  Here I’m toying around with Yahoo!’s search API.  You can also set the timeout attribute on the JSONP object if you want.  This is the number of seconds to wait for a response.  If no valid response comes back in that time, then the request is considered to have timed out and your onTimeout function will be called.

Now, there’s one flaw here that I just noticed: this code assumes the name of the parameter that tells the remote service the name of the callback function is itself named callback.  This is nearly always true of JSON-P services, but it doesn’t have to be.  I’ll probably make that update at some point, but I’m sure you can handle it yourself if need be 🙂

So, if you couple this approach with a JSON-P service that wraps the call in try…catch, then there’s only one situation left to deal with, and that’s HTTP error codes.  During my discussion, we all concluded there appears to be no way to deal with this until everyone is implementing JSONRequest.  This timeout technique though does give you a way to deal with them indirectly.  True enough, you still won’t be able to discern a 404 from a 500, but at least you can, after some time, handle not having gotten a response and thereby avoid hanging application UIs.

But hey, as a bunch of songs have said over the years: two out of three ain’t bad 🙂 I think these two tricks make JSON-P services a heck of a lot more robust, and that can only be a good thing for mashup builders everywhere!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.