Policy. While the protocol has been around for a few years, and is built into all of the major browsers, the protocol does not seem to be widely documented. Here are some experiences I’ve encountered while enabling access cross-origin access for JSON from a Rails server.
Server-side proxying is a traditional method for getting around the Same-Origin restriction for Ajax requests. With proxying, the owner of mysite.com implements a copy of the remote method that repeats the request from the page-owner’s own site. The server on “mysite.com” must process each remote method call by calling the method on “othersite.com” so that it can return the results to the browser.
http://mysite.com/method.json --> http://othersite.com/method.json
The great advantage of this method is that it works with any browser. The drawback is that it imposes work on web-service subscribers that seems redundant.
Cross-Origin Resource Sharing
A great introduction to the CORS protocol appears here:
and an example of the CORS transactions appears here.
The “Preflight” request is a new HTTP verb called OPTIONS. In a browser implementing CORS, each cross-origin GET or POST request is preceded by an OPTIONS request that checks whether the GET or POST is OK. If it is, the server must return some headers to allow the subsequent GET or POST. This is actually a wonderful capability. The server can allow or disallow remote access on a per-method basis, with access determined by HTTP referrer, IP or any other criterial.
The OPTIONS request contains Access-Control headers that are part of the CORS specification. The response must reply to these headers to allow the subsequent GET or POST to proceed.
For example, an access to
would be preceeded by an OPTIONS method that looks like this.
OPTIONS http://othersite.com/method.json Origin: http://mysite.com Access-Control-Request-Method: GET
The server would respond with an empty response body of type
“text/plain” that contains headers allowing the request.
Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Max-Age: 1728000 Content-Length: 0 Content-Type: text/plain
If your application uses non-standard headers, you must take special steps to permit them or the browser will flag a CORS violation. I ran into this restriction in the application I was writing. Unfortunately, the security violation messages from the browser are obscure, and it took me a while to figure this out.
X-Requested-With: XMLHttpRequest X-Prototype-Version: N.N.N.N
Our server must explicitly allow these headers in the CORS exchange or the browser will disallow the cross-origin request. The OPTIONS request will specify the headers it wants to add. Our OPTIONS/Response exchange looks like this.
OPTIONS http://othersite.com/method.json Origin: http://mysite.com Access-Control-Request-Method: GET Access-Control-Request-Headers: X-Requested-With, X-Prototype-Version
Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Headers: X-Requested-With, X-Prototype-Version Access-Control-Max-Age: 1728000 Content-Length: 0 Content-Type: text/plain
CORS in Rails
I implemented the CORS protocol in a Rails application with just a couple of filter methods added to my controller. Here they are. (If you want to follow this technique, you’ll need to make sure your routes allow access to HTTP “:options” methods.)
before_filter :cors_preflight_check after_filter :cors_set_access_control_headers # For all responses in this controller, return the CORS access control headers. def cors_set_access_control_headers headers['Access-Control-Allow-Origin'] = '*' headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS' headers['Access-Control-Max-Age'] = "1728000" end # If this is a preflight OPTIONS request, then short-circuit the # request, return only the necessary headers and return an empty # text/plain. def cors_preflight_check if request.method == :options headers['Access-Control-Allow-Origin'] = '*' headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS' headers['Access-Control-Allow-Headers'] = 'X-Requested-With, X-Prototype-Version' headers['Access-Control-Max-Age'] = '1728000' render :text => '', :content_type => 'text/plain' end end
The before_filter, cors_preflight_check is the last in my filter chain: earlier filters check for allowed access. If the request is for the OPTIONS method, then it short circuits the request, includes the necessary headers and returns a blank text body.
The after_filter, cors_set_access_control_headers, is for all requests that are returned by this controller. It includes the CORS headers for everything else.
CORS is implemented in all of the popular browsers, but client-side access seems to vary between IE and Safari/Chrome/Firefox. For me, it was interesting to see how server-side access control is not too different from what Adobe does for server-side policy files. It would be nice if these access controls could be unified, but I’m just happy to have them.