Supporting Cross-Domain AJAX in Rails using JSONP and CORS

Posted on by in Mobile, Web

The recent rise in popularity of client-side JavaScript MVC frameworks has led to a revival of web apps with thick clients. As more logic is moved to the client-side, the need to communicate with servers in different domains becomes more common. Unfortunately, JavaScript’s same origin policy prohibits you from making a request to a server in another domain. Two ways to circumvent this restriction are JSONP and CORS. In this article, we’ll take a look at supporting both of these in a Rails app.

JSONP

JSONP or “JSON with padding” is a technique that can be used to load JavaScript from a server in a different domain. JSONP takes advantage of the fact that JavaScript’s same origin policy doesn’t apply to the HTML <script> element. When using JSONP, the <script> element’s src attribute is set to a resource in another domain. This URL includes a callback parameter corresponding to a local JavaScript function. The server responds with JavaScript that calls this function, passing JSON as an argument. The client then evaluates this JavaScript, giving it access to the server’s data

In the following example, HTML from http://server1.example.com/ includes a <script> element to load JavaScript from a server in another domain, http://server2.example.com. The entire domain, including all subdomains, as well as the port number, are all used by JavaScript to determine if the server is in another domain. Here we have two apps at different subdomains within the same domain. The callback parameter is set to a local function defined in a previous <script> element.

http://server1.example.com/

The JavaScript returned from the server consists of a function call to parseUser, passing user #1 as JSON. Implementing this server-side logic in a Rails app is straightforward. ActionController::Base#render accepts a :callback option to specify the function to pass the resulting JSON to.

app/controllers/users_controller.rb

Let’s try this out (by running the Rails app locally) on the command line using curl.

DRYing up JSONP with Rack::JSONP

Repeating the same :callback option for multiple actions supporting JSONP quickly becomes tedious. rack-jsonp-middleware is a Ruby gem that includes a piece of Rack middleware that can take care of this repetition for us. For any JSONP request, rack-jsonp-middleware will strip the callback parameter value, forward the request on to Rails as if it were a JSON request, and then respond with JavaScript containing a function call to the callback parameter, passing it the returned JSON.

Using rack-jsonp-middleware will require a few changes to our client and server. Let’s first add the gem to our app’s Gemfile.

Gemfile

Then add it to the Rails middleware stack.

config/application.rb

rack-jsonp-middleware considers a request a JSONP request if the url ends in .jsonp. We’ll need to change the ending of our client url from .js to .jsonp.

http://server1.example.com/

rack-jsonp-middleware expects our server-side action to return JSON. Instead of handling JavaScript requests, let’s rewrite our action to return JSON. We can use ActionController::MimeResponds#respond_with and ActionController::MimeResponds.respond_to in our controller to simplify the implementation.

app/controllers/users_controller.rb

Testing this refactored version from the command line returns the same response as our previous implementation.

JSONP is a nice, simple solution for reading data from a server in another domain. But what if you want to write data? For that, there’s CORS.

CORS (Cross-Origin Resource Sharing)

CORS is a W3C standard, that specifies a way for a client to determine if a particular request can be made to a server in a different domain. With JSONP you’re limited to HTTP GET requests. CORS on the other hand, allows any type of request. It requires you to define who can do what to a given resource.

In CORS, the client first makes a “preflight” request to a server in another domain. This is an HTTP OPTIONS request, asking the server if the client can make a particular request. The “preflight” request includes CORS-specific headers specifying the client’s domain, the type of request they want to make (POST, GET, etc.), and any HTTP headers they want to send. The server response answers the request using CORS-specific headers. If the request is allowed, the client can then issue the cross-domain request.

Adding CORS support to a Rails app is easy with the rack-cors Ruby gem.

Supporting CORS in Rails with Rack::Cors

rack-cors handles CORS’s “preflight” requests by adding support for HTTP OPTIONS requests. It also includes a DSL for specifying on a per-resource basis, the allowable requesting domains, the types of requests, and the supported HTTP headers.

Let’s take a look at adding CORS support for updating and deleting users in our Rails app. The first step is to add rack-cors to our app’s Gemfile.

Gemfile

Then we’ll add it to the Rails middleware stack. This is where you configure your resources for CORS.

config/application.rb

Our configuration allows HTTP PUT and DELETE requests from http://server1.example.com to /users/\d+.json (we used a regular expression to match the user id in the URL). The allowable headers in such requests are:

  • Origin – the domain from where the request will be made (this is required by CORS).
  • Accept – the acceptable response media type.
  • Content-Type – the media type of the body of the request.

Let’s try this out from the command line using curl (this is the exact same CORS “preflight” request that jQuery will make when sending a cross-domain AJAX request in a browser).

This “preflight” request uses several CORS-specific headers:

  • Origin – the domain from where the request will be made.
  • Access-Control-Request-Headers – a list of headers we want to send with our request.
  • Access-Control-Request-Method – the type of request we want to make.

Essentially, this “preflight” request is asking the server at http://localhost:3000 if we can send an update from
http://server1.example.com to one of its users.

The server response also includes several CORS-specific headers:

  • Access-Control-Allow-Origin – the domains that are allowed to make a request to this resource (a value of “*” is a wildcard that matches any domain).
  • Access-Control-Allow-Methods – the types of requests that are allowed (both PUT and DELETE in the above response).
  • Access-Control-Allow-Headers – the allowable request headers.

rack-cors has also added some additional CORS-specific headers with default values:

  • Access-Control-Expose-Headers – a list of additional response headers the client can access. By default, the client can only access simple response headers such as Content-Type and Last-Modified. rack-cors defaults this header to an empty list.
  • Access-Control-Max-Age – how long (in seconds) the client can cache this “preflight” response. rack-cors defaults this to 20 days.
  • Access-Control-Allow-Credentials – tells the client whether it should send cookies in CORS requests. rack-cors defaults this to true.

Browser Support for CORS

According to Wikipedia, CORS is currently supported by all major browsers (IE 8+) except Opera.

I recently used CORS to support an internal app. This meant only supporting Chrome, Firefox, and Safari. So far, I’ve had no problems using CORS in any of these browsers.

Fine-grained Access Control

The Access-Control-Allow-Origin header in CORS, gives us more flexibility and security than JSONP. Instead of opening up a resource to any domain using JSONP, we can create a whitelist of allowable domains, HTTP methods, and headers.

The Rise of Thick Clients

As client-side JavaScript MVC frameworks continue to rise in popularity, the same origin request policy becomes far too limiting. JSONP offers a simple workaround for reading data from servers in other domains. CORS is more complex, but it provides the ability to not only read but also write data.

JSON APIs are already trivial to implement in Rails. And thanks to Ruby gems, such as rack-jsonp-middleware and rack-cors, opening them up to thick client apps running on other domains is just as easy.


Feedback

  Comments: 13

  1. Michael Wynholds


    First of all… I find it a little crazy that Opera doesn’t support CORS! Opera tends to be very up to date with standards, so I’m surprised at this. Do you know if this was done for a reason or if it was just an oversight?

    Outside of the Opera thing, it seems to me that CORS is a much better way to go. JSONP requires changes on both client and server, and only supports GETs. CORS requires only configuration on the server, is much more fine-grained about security, and supports all HTTP methods. Plus its acronym sounds way cooler.

    Do you know of any integration testing support or tools or patterns to test CORS requests?

    -mike


  2. It seems like CORS makes the most sense overall. The only downsides I see are that you are doubling requests to the server and lack of support for Opera. For a public API, would these be big enough reasons for you to use JSONP instead?


    • The server can use the Access-Control-Max-Age response header to tell the client to cache the “preflight” request response for a period of time (in seconds). This helps eliminate the extra “preflight” request for future cross-domain requests.

      I don’t think the extra “preflight” request would be enough to convince me to go with JSONP instead.


  3. Was going to put cors on the back burner, but thanks for sharing rack/cors will take another look at it


  4. +1. Successfully used the CORS solution in a Rails 3 based service hosted on Heroku.


  5. Excellent post, Jared – thanks for saving me a lot of time!


  6. I really feel strongly about it and also love learning more on that topic. If achievable, while you gain competence, would you mind updating your blog with more information? It is really helpful for me

    fm transmitter for phone


  7. This seems quite simple but I can’t get either method to work to save my life. Several outcome scenarios I’ve run into:

    1. Client’s ajax request returning 200 OK, with the JSON I want in the Response. At that point, either I can’t access the response, the jQuery callback I am using goes to error, or I return a funky JS function that really makes no sense.

    2. Expected Access-Control-Allow-Origin error.

    3. Nothing at all.

    Server side: Rails Server Running on Heroku. (I’ve also tried localhost)

    Client side: One simple index.html page with jQuery embedded. I am making the calls from within Script tags.

    I’m against the wall. Does anyone have any ideas for how to troubleshoot this?

Your feedback