Headers in nginx — a tale of woe

Jamie Wiseman
Code Enigma
Published in
4 min readApr 27, 2018

A colleague came to me this week with an interesting problem. They were trying to set CORS headers for a client website, but only for a specific path on a Drupal 8 website. The puzzle, as it turns out, was compounded by the fact that these headers were already set for one path, but for this other one, the headers would not appear.

Down the rabbit hole I went. The existing, working path’s location block was found in the site’s vhost and looked like this:

location ~ /my-javascript-widget(.*)$ {
add_header Content-Type application/x-javascript;
alias /var/www/literal/path/to/my/js/jquery.widget.js;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#
# Custom headers and headers various browsers *should* be OK with but aren't
#
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
#
# Tell client that this pre-flight info is valid for 20 days
#
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
add_header Content-Type application/x-javascript;
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
add_header Content-Type application/x-javascript;
}
}

It’s a little hard to see with the line wrapping, but all this is doing is setting different headers on a path, depending on the request method used. All good. And this works, so the same principle should work for our path in Drupal, right?

Well… kind of. The difference between our path here and Drupal, is that this one is aliased, so this is the final location block before the file is fetched from the aliased path. Nginx has a quirk, in that if you’re adding headers in a location block, they will only get added in the last location block before the backend. Our Drupal config uses several successive location blocks before PHP is called, so that we can mask different URLs onto the ?q variable:

location / {
# This is cool because no php is touched for static content
try_files $uri /index.php?$query_string;
}
# Accepted .php files
location ~ ^/(index|cron|path/to/other/accepted/php/file)\.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_intercept_errors on;
}

..all of which means that we’d have to do our stuff in the second of these. Simple!

Almost. We’re already in a location block, so we can’t match the path with the location. But that’s okay! Nginx has a variable called $request_uri which we can use to know what path has been requested, and apply our headers only for that path:

# Accepted .php files
location ~ ^/(index|cron|path/to/other/accepted/php/file)\.php$ {
set $cors FALSE;
if ($request_uri ~ ^\/our-cors-path(.*)$) {
set $cors $request_method;
}
if ($cors = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#
# Custom headers and headers various browsers *should* be OK with but aren't
#
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
#
# Tell client that this pre-flight info is valid for 20 days
#
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
if ($cors = 'POST') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
add_header Content-Type application/x-javascript;
}
if ($cors = 'GET') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
add_header Content-Type application/json;
}
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_intercept_errors on;
}

Again with the wrapping, Medium!

So what we’ve had to do here is query the URI, and set a variable to be the incoming request method ONLY for that URI. For any other case, our variable ($cors) will be FALSE. Then immediately after, we check the $cors variable, and if it’s one of our request methods (OPTIONS, POST or GET), we add headers. Finally, we hand off to fastcgi to execute the relevant PHP to return a JSON feed.

It’s important to note that this method is used because Nginx will not allow nesting of if() statements.

Hope this helps someone else!

--

--