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.