Stripe.js and JSONP

Alex MacCaw, September 7, 2012

We recently shipped a new version of Stripe.js, the JavaScript library behind pretty much every Stripe transaction. The library is in charge of taking credit card data, submitting it to Stripe’s servers and then returning a token which can be charged. This rewrite of Stripe.js comes with a bunch of new improvements, and all existing Stripe.js users have been automatically upgraded behind the scenes.

Stripe.js’s history is an interesting example of how the web works in practice: technologies being repurposed for unexpected use-cases. I thought it’d be interesting to illustrate how this played out.

iframe

Initially, when we were building Stripe.js, we implemented network calls using iframes. Iframes were, of course, never intended for use with cross-domain requests. However, HTML5 added support for postMessage, which enables two cooperating pages to communicate. This isn't quite enough, though—IE6, as usual, doesn’t support postMessage. It turns out that you can still make the iframe hack work by using a non-obvious shared channel: the iframe’s src property—and, in particular, the anchor fragment.

This is what the first version of Stripe.js did. What it lacked in elegance it made up for in compatibility. Still, it meant that we had to serve our iframe code from api.stripe.com, which was somewhat inelegant. The underlying code was hacky and awkward to maintain.

CORS

The standards of the web progress slowly, but they do progress. Back in 2005, a couple of people from Tellme Networks wrote a W3C working group note with the catchy title of Authorizing Read Access to XML Content Using the <?access-control?> Processing Instruction 1.0, which introduced a concept of access control declarations to XML and HTTP. This went through a few versions over the years (by 2007, it was Access Control for Cross-site Requests). Today, this has become CORS, or Cross-Origin Resource Sharing.

Supporting CORS is pretty simple—a matter of adding a few HTTP headers. Browsers will automatically prefix Ajax requests to third party servers with a OPTIONS request, verify that the CORS headers are present and valid, and then send the actual request.

CORS has begun to achieve widespread adoption: most major browsers now support it, Amazon just added CORS support to S3, and YouTube turned it on a few months ago.

With the rise of JavaScript applications, it’s clear that all APIs should now support CORS: if you’re providing an HTTP API, it’d be strange not to support the primary language of the web. As such, we’ve recently enabled CORS support in Stripe’s API, and anyone can now make cross-origin requests to Stripe.

Unfortunately, that’s not quite enough for Stripe.js. IE6 and IE7 both lack CORS support, while IE8 and IE9 have broken implementations. IE10 is the only version with a non-buggy CORS implementation. Obviously, compatibility is paramount for Stripe.js — we want to support all major browsers, right down to IE6—and so we needed to look elsewhere.

JSONP

And so we return to using web technologies in unintended ways: JSONP. JSONP is a really neat and simple hack, and works in pretty much every browser under the sun. It involves creating a <script> tag that loads an API endpoint, and which in turn returns some JSON wrapped in a function call.

We decided to use JSONP for the Stripe.js rewrite. Adding support to our API took a few steps. First, we had to ensure that any responses to requests with callback query parameters were wrapped in a JSONP callback. Next, we had to make sure that JSONP responses always returned a 200 HTTP status code, with the real status code present in the response body. Lastly, since JSONP only supports GET requests, but our API uses a variety of request methods, we had to implement HTTP method override support with a _method query parameter. Rather than clutter our API code, we implemented all of this as middleware atop the API logic itself.

With this in place, we rewrote the Stripe.js client library in CoffeeScript, and conducted a huge amount of testing in every browser we support.

Advantages

So, at the end of the day, what are the advantages of this new release?

First off, Stripe.js is now about half its previous size, which saves time and bandwidth for our users. It now works when loaded with file:// URLs, which was a frequent complaint of those hosting development locally.

On the Stripe end, we were able to eliminate a lot of complexity and code required to support iframe tunneling. All in all, a pretty good refactor.