Tuesday, July 26, 2011

JSONP - give it a REST!

The limitations of JSONP make it especially unsuited for use with RESTful web services
  1.  limited to GET
  2.  can't handle HTTP status <> 200
  3.  size limitation on GET querystring


And yet, many web applications have a need to access REST-style ( I like to refer to ours as RESTive ) web services cross-origin. JSONP is often used for this. I'm sure there are cases where a developer's hands are tied, but often this is more likely an example of the hammer and nails phenomenon.

In any case, there's no reason to let it infect your server-side code.

I've seen a number of schemes to support JSONP by passing parameters to tell the server that in this case GET really means POST, or PUT or DELETE. Heinous! Don't get me wrong, I'm not some sort of REST zealot. It's a useful fiction at best. But adding unnecessary code is *always* bad.

All you need is a little varnish to overcome (1) and (2). I can't help with (3), sorry.
(It's actually a deal-breaker for us, with some APIs requiring large amounts of POST data.)

First, we need some indicator that the request is JSONP.  In this example I chose to prepend /jsonp/<desired method> to the request URL. Next, since we're going to rewrite the URL, we need a new indicator to remember that it was a JSONP request. I chose the custom header req.http.x-rest-method. Then we need to remove the prepended string from the URL to get the request to the correct location. (This would all be so much simpler if we could use custom headers when making a JSONP request in the first place. We'd need only the header and wouldn't have to screw with the URL at all.)

sub vcl_recv { 
        if (req.url ~ "^/jsonp/get/") { 
                set req.request = "GET"; 
                set req.http.x-rest-method = "JSONP"; 
                set req.url = regsub(req.url, "^/jsonp/get", ""); 
        } 
        if (req.url ~ "^/jsonp/put/") { 
                set req.request = "PUT"; 
                set req.http.x-rest-method = "JSONP"; 
                set req.url = regsub(req.url, "^/jsonp/put", ""); 
        } 
        if (req.url ~ "^/jsonp/post/") { 
                set req.request = "POST"; 
                set req.http.x-rest-method = "JSONP"; 
                set req.url = regsub(req.url, "^/jsonp/post", ""); 
        } 
        if (req.url ~ "^/jsonp/delete/") { 
                set req.request = "DELETE"; 
                set req.http.x-rest-method = "JSONP"; 
                set req.url = regsub(req.url, "^/jsonp/delete", ""); 
        } 
} 

So now the server will see the request just as it would from any other client, blissfully unaware that it had unclean origins. Well, there's still the parameters-as-GET-querystring issue. That may be problematic for some. If your server is written using software that allows you not to care where the parameters came from, e.g.
Catalyst::Request->params, or $_REQUEST in php, you're golden.

So now for (2):

sub vcl_deliver { 
        if (req.http.x-rest-method ~ "JSONP") { 
                set resp.status = 200; 
        } 
} 

If the request originated from JSONP, we fix the HTTP status of the response to 200, whatever it was. This implies that you're providing the client some sort of indicator of error in the body. But shouldn't you be anyway?

Of course, with varnish, you could just make the REST API look same-origin and obviate JSONP:

backend default { 
  .host = "page.mysite.com"; 
  .port = "80"; 
} 
 
 
 
backend myapi { 
  .host = "myapi.services.mysite.com"; 
  .port = "80"; 
} 
 
 
 
sub vcl_recv { 
        if (req.url ~ "^/myapi/") { 
                set req.backend = myapi; 
                set req.url = regsub(req.url, "^/myapi", ""); 
        } 
} 
 
In this example, I've prepended the name of the API to the URL. It appears to the requester as page.mysite.com/myapi, but is routed to myapi.services.mysite.com/ by varnish.

If these proxy layer solutions are not an option, try something like easyXDM. It will probably make your Javascript cleaner, and still protects the integrity of the service-layer code.

No comments:

Post a Comment