If you haven’t heard of CORS, it stands for Cross Origin Resource Sharing; it enables you to request data from another domain using JavaScript in the browser. Before CORS you would do something using JSONP or iframes or something else horrible.

Now that almost all APIs require CORS to be setup, and with the intricacies of how people design their API infrastructure leading to the use of HTTP proxies like nginx there are now a few reaons to move handling of your CORS rules out to the HTTP Proxy.

We recently wrote about using nginx to reply to the client with JSON even if the upstream API process wasn’t available. We talked in that post about the befits of doing this, but without the changes we’re abut to talk about - users accessing your data from another domain still wouldn’t be able to access the data.

This is because your API process had been providing the relevant CORS headers but now that it’s not available, and nginx is responding instead, it isn’t giving back those vital CORS headers. To fix this, it’s best to just move all CORS handling out to nginx.

The enable-cors.org website gives you some basic configurations for many different server types but we’ve expanded on the nginx example.

Firstly, we’ll want to add a handler for the preflight OPTIONS request within the location directive:

server {
    listen 80;

    server_name domain.tld;

    location / {

        if ($http_origin) {
            set $cors "true";
        }

        if ($request_method = 'OPTIONS') {
            set $cors "${cors}options";
        }

        if ($request_method = 'GET') {
            set $cors "${cors}nonoptions";
        }

        if ($request_method = 'POST') {
            set $cors "${cors}nonoptions";
        }

        if ($request_method = 'PUT') {
            set $cors "${cors}nonoptions";
        }

        if ($request_method = 'DELETE') {
            set $cors "${cors}nonoptions";
        }

        if ($cors = "truenonoptions") {
            more_set_headers 'Access-Control-Allow-Origin: $http_origin';
            more_set_headers 'Access-Control-Allow-Credentials: true';
            more_set_headers 'Access-Control-Allow-Headers: Content-Type, Accept, Accept-Encoding, Accept-Language, Connection, Host, Referer, Origin, User-Agent, Cache-Control, Keep-Alive, X-Requested-With, If-Modified-Since';
            more_set_headers 'Access-Control-Expose-Headers: Request-Id';            
        }

        if ($cors = "trueoptions") {
            #more_set_headers 'Access-Control-Max-Age: 1728000';
            more_set_headers 'Access-Control-Allow-Origin: $http_origin';
            more_set_headers 'Access-Control-Allow-Credentials: true';
            more_set_headers 'Access-Control-Allow-Methods: GET, PUT, POST, DELETE';
            more_set_headers 'Access-Control-Allow-Headers: ';
            more_set_headers 'Content-Length: 0';
            more_set_headers 'Content-Type: text/plain charset=UTF-8';
            return 204;
        }

        proxy_pass http://127.0.0.1:3000;
        proxy_redirect off;

        proxy_http_version 1.1;
    }

Now if we break this up, you’ll notice a string variable being formed:

if ($http_origin) {
  set $cors "true";
}

if ($request_method = 'OPTIONS') {
  set $cors "${cors}options";
}

if ($request_method = 'GET') {
  set $cors "${cors}nonoptions";
}

if ($request_method = 'POST') {
  set $cors "${cors}nonoptions";
}

if ($request_method = 'PUT') {
  set $cors "${cors}nonoptions";
}

if ($request_method = 'DELETE') {
  set $cors "${cors}nonoptions";
}

This is because of how if statements work within the nginx configuration file.

Then using the resulting variable, we can change the result of the request

if ($cors = "truenonoptions") {
  more_set_headers 'Access-Control-Allow-Origin: $http_origin';
  more_set_headers 'Access-Control-Allow-Credentials: true';
  more_set_headers 'Access-Control-Allow-Headers: Content-Type, Accept, Accept-Encoding, Accept-Language, Connection, Host, Referer, Origin, User-Agent, Cache-Control, Keep-Alive, X-Requested-With, If-Modified-Since';
  more_set_headers 'Access-Control-Expose-Headers: Request-Id';            
}

if ($cors = "trueoptions") {
  #more_set_headers 'Access-Control-Max-Age: 1728000';
  more_set_headers 'Access-Control-Allow-Origin: $http_origin';
  more_set_headers 'Access-Control-Allow-Credentials: true';
  more_set_headers 'Access-Control-Allow-Methods: GET, PUT, POST, DELETE';
  more_set_headers 'Access-Control-Allow-Headers: Content-Type, Accept, Accept-Encoding, Accept-Language, Connection, Host, Referer, Origin, User-Agent, Cache-Control, Keep-Alive, X-Requested-With, If-Modified-Since';
  more_set_headers 'Content-Length: 0';
  more_set_headers 'Content-Type: text/plain charset=UTF-8';
  return 204;
}

You’ll see that if the $cors variable is set to truenonoptions we just add some headers to the response; but if the $cors variable is set to trueoptions we terminate the request early with a 204 response. This also serves to protect your upstream API process from having to deal with OPTIONS requests.


Now all you need to do is add the same block of CORS configuration to the errors configuration we added in our previous post about adding JSON error responses to nginx.

location ^~ /errors/ {
   internal;

   if ($http_origin) {
       set $cors "true";
   }

   if ($request_method = 'OPTIONS') {
       set $cors "${cors}options";
   }

   if ($request_method = 'GET') {
       set $cors "${cors}nonoptions";
   }

   if ($request_method = 'POST') {
       set $cors "${cors}nonoptions";
   }

   if ($request_method = 'PUT') {
       set $cors "${cors}nonoptions";
   }

   if ($request_method = 'DELETE') {
       set $cors "${cors}nonoptions";
   }

   if ($cors = "truenonoptions") {
       more_set_headers 'Access-Control-Allow-Origin: $http_origin';
       more_set_headers 'Access-Control-Allow-Credentials: true';
       more_set_headers 'Access-Control-Allow-Headers: Content-Type, Accept, Accept-Encoding, Accept-Language, Connection, Host, Referer, Origin, User-Agent, Cache-Control, Keep-Alive, X-Requested-With, If-Modified-Since';
   }

   if ($cors = "trueoptions") {
       # Tell browser to cache this pre-flight info for 20 days - commented out right now
       #more_set_headers 'Access-Control-Max-Age: 1728000';

       more_set_headers 'Access-Control-Allow-Origin: $http_origin';
       more_set_headers 'Access-Control-Allow-Credentials: true';
       more_set_headers 'Access-Control-Allow-Methods: GET, PUT, POST, DELETE';
       more_set_headers 'Access-Control-Allow-Headers: Content-Type, Accept, Accept-Encoding, Accept-Language, Connection, Host, Referer, Origin, User-Agent, Cache-Control, Keep-Alive, X-Requested-With, If-Modified-Since';
       more_set_headers 'Content-Length: 0';
       more_set_headers 'Content-Type: text/plain charset=UTF-8';
       return 204;
   }

   root   /etc/nginx/static-files/;
   more_set_headers 'Content-Type: application/json charset=UTF-8';
}

Now you’ve got an nginx configuration that takes all preflight OPTIONS requests away from your upstream API process as well as giving back error responses with the same CORS information so that they can be understood by those JavaScript originated requests from a different domain; which let’s face it, is probably most of your API traffic.