nginx add_header Inheritance: Why Location Blocks Override Server Headers and How to Fix It


1 views

When working with nginx configurations, many developers encounter a puzzling behavior: headers defined at the server level mysteriously disappear when you add headers within location blocks. Let's examine this with concrete examples.

server {
    listen       80;
    server_name  localhost;
    root /var/www;
    add_header X-Server-Header "server_value";
    add_header Cache-Control "public, max-age=3600";

    location / {
        return 200 "Basic config works";
    }
}

This configuration correctly sends both headers in responses.

Now let's modify the location block:

server {
    listen       80;
    server_name  localhost;
    root /var/www;
    add_header X-Server-Header "server_value";
    add_header Cache-Control "public, max-age=3600";

    location / {
        add_header X-Location-Header "location_value";
        return 200 "Problem demonstration";
    }
}

Surprisingly, only X-Location-Header appears in responses - the server-level headers disappear.

This isn't a bug but rather nginx's designed behavior. The add_header directive follows inheritance rules where:

  • Location blocks completely override server-level headers
  • Headers aren't merged or inherited
  • The last matching add_header directive wins

Here are several approaches to maintain both server and location headers:

1. Duplicate Headers

location / {
    add_header X-Server-Header "server_value";
    add_header Cache-Control "public, max-age=3600";
    add_header X-Location-Header "location_value";
    return 200 "Duplicate headers solution";
}

2. Use include files

# In headers.conf
add_header X-Server-Header "server_value";
add_header Cache-Control "public, max-age=3600";

# In nginx.conf
location / {
    include headers.conf;
    add_header X-Location-Header "location_value";
    return 200 "Include files solution";
}

3. Use map blocks

map $uri $extra_headers {
    default "";
    "/special" "X-Special: value";
}

server {
    ...
    location / {
        add_header X-Base-Header "base";
        add_header X-Extra $extra_headers;
    }
}

For complex scenarios, consider the third-party module:

load_module modules/ngx_http_headers_more_filter_module.so;

server {
    ...
    more_set_headers "Server-Header: value";

    location / {
        more_set_headers "Location-Header: value";
        # Original server headers remain
    }
}
  • Place common headers in server context when possible
  • Use include files for header maintenance
  • Document header inheritance in team documentation
  • Consider using the headers_more module for complex requirements

When working with Nginx's add_header directive, many developers encounter an unexpected behavior where headers defined in location blocks completely override those defined in server blocks. Let's examine this through practical examples.

Consider this basic server configuration:

server {
    listen       80;
    server_name  example.com;
    add_header X-Frame-Options "DENY";
    add_header X-Content-Type-Options "nosniff";

    location / {
        add_header Cache-Control "public, max-age=3600";
        return 200 "Hello World";
    }
}

The surprising result is that only Cache-Control header appears in responses to /, while the security headers from the server block disappear.

This behavior is actually by design in Nginx. Unlike other directives that inherit from outer blocks, add_header follows these rules:

  • Headers defined in a location block completely replace any headers with the same names from higher levels
  • If you define any headers in a location block, all headers from server/HTTP blocks are discarded

Here are three approaches to maintain headers across configurations:

1. Explicit Re-definition

The simplest solution is to redeclare all necessary headers in each location:

location / {
    add_header X-Frame-Options "DENY";
    add_header X-Content-Type-Options "nosniff";
    add_header Cache-Control "public, max-age=3600";
    # Other directives...
}

2. Using include Files

For better maintainability, use includes for common headers:

location / {
    include /etc/nginx/security_headers.conf;
    add_header Cache-Control "public, max-age=3600";
}

Where security_headers.conf contains:

add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options "nosniff";

3. Advanced: Headers More Module

For complex scenarios, consider the third-party headers-more-nginx-module:

more_set_headers "X-Frame-Options: DENY";
more_set_headers "X-Content-Type-Options: nosniff";

location / {
    more_set_headers "Cache-Control: public, max-age=3600";
}
  • Group related headers logically (security, caching, CORS)
  • Document header purposes in comments
  • Consider performance impact when adding multiple headers
  • Test header behavior with tools like curl or browser dev tools

Here's a production-ready configuration that maintains security headers while allowing location-specific overrides:

# Global security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

server {
    listen 443 ssl;
    server_name api.example.com;
    
    # API specific headers
    location /v1/ {
        add_header Cache-Control "no-store";
        add_header X-API-Version "1.0";
        # Security headers remain via 'always' flag
    }
    
    location /static/ {
        add_header Cache-Control "public, max-age=31536000";
    }
}

Note the use of the always parameter in newer Nginx versions to force header inclusion even in error responses.