Cross Origin Resource Sharing (CORS) with nginx / chrome

I have a website with the following segmentation:

api.example.com 
developers.example.com 
example.com

I would like to allow both example.com and developers.example.com to make AJAX requests to api.example.com.

My nginx configuration so far for api.example.com, which is a Rack app being served by unicorn, looks like this:

upstream app_server {
  server unix:/tmp/api.example.com.sock fail_timeout=0;
}

server {
       listen 80;
       server_name api.example.com;
       access_log /home/nginx/api.example.com/log/access.log;
       error_log /home/nginx/api.example.com/log/error.log;
       location / {
         add_header 'Access-Control-Allow-Origin' 'http://example.com,http://developers.example.com';
         add_header 'Access-Control-Allow-Credentials' 'true';
         add_header 'Access-Control-Allow-Headers' 'Content-Type,Accept';
         add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';

         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header Host $http_host;
         proxy_redirect off;
         proxy_pass http://app_server;
       }

}

Based on my reading, this should be sufficient for what I'm trying to do.

The OPTIONS response:

HTTP/1.1 200 OK
Server: nginx/0.7.67
Date: Sat, 28 Apr 2012 17:20:08 GMT
Content-Type: application/json
Connection: close
Status: 200 OK
Content-Length: 0
Access-Control-Allow-Origin: http://developers.example.com,http://example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type,Accept
Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE

But when I try the following in the Chrome console:

$.ajax("http://api.example.com", {
  type: 'get',
  contentType: "application/json",
  accept: "application/json"
}).success(function(data){
  console.log("success!", data);
}).fail(function(jqxhr, statusText){
  console.log("fail!", jqxhr, statusText);
})

I see:

XMLHttpRequest cannot load http://api.example.com/. Origin
http://developers.example.com is not allowed by Access-Control-Allow-Origin.

And the same for http://example.com.

What am I missing?

If I set the Access-Control-Allow-Origin to * then I see:

HTTP/1.1 200 OK
Server: nginx/0.7.67
Date: Sat, 28 Apr 2012 17:28:41 GMT
Content-Type: application/json
Connection: close
Status: 200 OK
Content-Length: 0
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type,Accept
Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE

But the jQuery request still fails, with chrome also highlighting that the pre-flight OPTIONS failed (even though it returned 200 OK).


Solution 1:

According to the CORS spec multiple origins should be separated by spaces, not commas as you've used, so try sending this header:

Access-Control-Allow-Origin: http://developers.example.com http://example.com

The Mozilla documentation doesn't mention multiple origins though, so if that still doesn't work try only sending:

Access-Control-Allow-Origin: http://developers.example.com

If that works you'll need to configure nginx or your application server to return an Access-Control-Allow-Origin header containing the value of the Origin header sent by the client if it matches the allowed list. Something like the following (untested) nginx configuration could do that:

if ($http_origin ~ "^(http://developers.example.com|http://example.com)$") {
    add_header "Access-Control-Allow-Origin" $http_origin;
}

Solution 2:

Using an if in a location block in an nginx configuration like this:

if ($http_origin ~ "^(http://developers.example.com|http://example.com)$") {
    add_header "Access-Control-Allow-Origin" $http_origin;
}

Cause nginx to do weird things. Specifically, proxy_pass and try_files don't work as expected. See http://wiki.nginx.org/IfIsEvil for more info.