90

I am having an intriguing problem where whenever I use add_header in my virtual host configuration on an ubuntu server running nginx with PHP and php-fpm it simply doesn't work and I have no idea what I am doing wrong. Here is my config file:

server {
    listen   80; ## listen for ipv4; this line is default and implied
    #listen   [::]:80 default ipv6only=on; ## listen for ipv6

    root /var/www/example.com/webroot/;
    index index.html index.htm index.php;

    # Make site accessible from http://www.example.com/
    server_name www.example.com;

    # max request size
    client_max_body_size 20m;

    # enable gzip compression
    gzip             on;
    gzip_static      on;
    gzip_min_length  1000;
    gzip_proxied     expired no-cache no-store private auth;
    gzip_types       text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript;

    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Credentials' 'true';
    add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
    add_header PS 1

    location / {
            # First attempt to serve request as file, then
            # as directory, then fall back to index.html
            try_files $uri $uri/ /index.php?$query_string;
            # Uncomment to enable naxsi on this location
            # include /etc/nginx/naxsi.rules
    }


    location ~* \.(css|js|asf|asx|wax|wmv|wmx|avi|bmp|class|divx|doc|docx|eot|exe|gif|gz|gzip|ico|jpg|jpeg|jpe|mdb|mid|midi|mov|qt|mp3|m4a|mp4|m4v|mpeg|mpg|mpe|mpp|odb|odc|odf|odg|odp|ods|odt|ogg|ogv|$
            # 1 year -> 31536000
            expires 500s;
            access_log off;
            log_not_found off;
            add_header Pragma public;
            add_header Cache-Control "max-age=31536000, public";
    }
    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    location ~ \.php$ {
            fastcgi_split_path_info ^(.+\.php)(/.+)$;
            # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini

            # With php5-cgi alone:
            #fastcgi_pass 127.0.0.1:9000;
            # With php5-fpm:
            fastcgi_pass unix:/var/run/example.sock;
            fastcgi_index index.php?$query_string;
            include fastcgi_params;

            # instead I want to get the value from Origin request header
    }

    # Deny access to hidden files
    location ~ /\. {
            deny all;
            access_log off;
            log_not_found off;
    }

    error_page 403 /403/;
}

server {
    listen 80;
    server_name example.com;
    rewrite     ^ http://www.example.com$request_uri? permanent;
}

I've tried adding the headers to the other location sections but the result is the same.

Any help appreciated!!

1

7 Answers 7

143

There were two issues for me.

One is that nginx only processes the last add_header it spots down a tree. So if you have an add_header in the server context, then another in the location nested context, it will only process the add_header directive inside the location context. Only the deepest context.

From the NGINX docs on add_header:

There could be several add_header directives. These directives are inherited from the previous level if and only if there are no add_header directives defined on the current level.

Second issue was that the location / {} block I had in place was actually sending nginx to the other location ~* (\.php)$ block (because it would repath all requests through index.php, and that actually makes nginx process this php block). So, my add_header directives inside the first location directive were useless, and it started working after I put all the directives I needed inside the php location directive.

Finally, here's my working configuration to allow CORS in the context of an MVC framework called Laravel (you could change this easily to fit any PHP framework that has index.php as a single entry point for all requests).

server {
    root /path/to/app/public;
    index index.php;

    server_name test.dev;

    # redirection to index.php
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini

        # With php5-fpm:
        fastcgi_pass unix:/var/run/php5-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;

        # cors configuration
        # whitelist of allowed domains, via a regular expression
        # if ($http_origin ~* (http://localhost(:[0-9]+)?)) {
        if ($http_origin ~* .*) { # yeah, for local development. tailor your regex as needed
             set $cors "true";
        }

        # apparently, the following three if statements create a flag for "compound conditions"
        if ($request_method = OPTIONS) {
            set $cors "${cors}options";
        }

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

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

        # now process the flag
        if ($cors = 'trueget') {
            add_header 'Access-Control-Allow-Origin' "$http_origin";
            add_header 'Access-Control-Allow-Credentials' 'true';
        }

        if ($cors = 'truepost') {
            add_header 'Access-Control-Allow-Origin' "$http_origin";
            add_header 'Access-Control-Allow-Credentials' 'true';
        }

        if ($cors = 'trueoptions') {
            add_header 'Access-Control-Allow-Origin' "$http_origin";
            add_header 'Access-Control-Allow-Credentials' 'true';

            add_header 'Access-Control-Max-Age' 1728000; # cache preflight value for 20 days
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since';

            add_header 'Content-Length' 0;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            return 204;
        }
    }

    error_log /var/log/nginx/test.dev.error.log;
    access_log /var/log/nginx/test.dev.access.log;
}

The gist for the above is at: https://gist.github.com/adityamenon/6753574

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks, this helped me fix my issue. I had added a header in nginx.conf, but it wasn't taking. After reading this, I moved the add_header to the specific sites_available config file and then it worked.
The index.php route handling was what caught me out too. Thanks!
I had similar issue as well. Just to add something important: the same rule is valid inside if statements as well - you have to add all the "add_header" statements there
This literally saved me from doing a nasty hack on some php files. Thank you!
Do use something like github.com/yandex/gixy to validate your nginx.conf - shows which header is getting dropped etc.
47

I had the issue of not getting the response header due to the response code not within the allowed range, unless you specify the "always" keyword after the header value.

From the official docs:

Adds the specified field to a response header provided that the response code equals 200, 201, 204, 206, 301, 302, 303, 304, 307, or 308. The value can contain variables.

Comments

31
+25

When I test the above add_header settings with:

# nginx -t && service nginx reload

I get

nginx: [emerg] directive "add_header" is not terminated by ";" in
/etc/nginx/enabled-sites/example.com.conf:21

nginx: configuration file /etc/nginx/nginx.conf test failed

So the complain is reagarding this line:

add_header PS 1

missing the semi-colon (;)

To test the headers I like to use

# curl -I http://example.com

According to the ngx_http_headers_module manual

syntax: add_header name value;
default:     —
context:    http, server, location, if in location

I further tried

add_header X-test-A 1;
add_header "X-test-B" "2";
add_header 'X-test-C' '3';

in the context of http, server and location, but it only showed up in the server context.

Comments

8

Firstly, let me say that after looking around the web, I found this answer popping up everywhere:

location ~* \.(eot|ttf|woff|woff2)$ {
    add_header Access-Control-Allow-Origin *;
}

However, I have decided to answer this question with a separate answer as I only managed to get this particular solution working after putting in about ten more hours looking for a solution.

It seems that Nginx doesn't define any [correct] font MIME types by default. By following this tuorial I found I could add the following:

application/x-font-ttf           ttc ttf;
application/x-font-otf           otf;
application/font-woff            woff;
application/font-woff2           woff2;
application/vnd.ms-fontobject    eot;

To my etc/nginx/mime.types file. As stated, the above solution then worked. Obviously, this answer is aimed at sharing fonts but it's worth noting that the MIME types may not be defined in Nginx.

1 Comment

Setting the MIME types solved the issue. Thanks @DazBaldwin.
1

Evidently the add_header inheritance quirk/gotcha applies to the upstream layer as well.

I had a script pre-authorizing requests meant for another service, and was therefore returning all of the headers from the other service.

Once I started adding an 'Access-Control-Allow-Origin' entry along with these relayed headers, the browser would actually get the entry and allow the request.

Comments

0

I don't think it works properly by reloading ==> nginx -s reload

When I used add_header and then reloaded, nothing changed in response. But when I made a deliberate error and saw a 404 error on the client side, and then fixed my deliberate error and reloaded again, Add_header worked.

Comments

0

Check if you add header in 80 server block, however use https 443 link.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.