Nginx add_header not working, headers not showing up or disappearing in response

Written by - 0 comments

Published on - last updated on April 22nd 2022 - Listed in Nginx Internet


Nginx's header module allows to add specific headers (add_header) to a server response. This can be pretty helpful, for example to add security related headers.

The following example shows an additional header in the server context:

server {
  listen 443 ssl http2;
  server_name api.example.com;
  access_log /var/log/nginx/api.example.com.access.log;
  error_log /var/log/nginx/api.example.com.error.log;

[... ssl settings ... ]

  add_header Strict-Transport-Security max-age=2678400;

  location / {
    proxy_pass http://127.0.0.1:8080;
  }

}

It's the response code's fault!

But in certain situations the header is not shown in the response:

ck@mint ~ $ curl -I https://api.example.com
HTTP/2 404
server: nginx
date: Tue, 08 Feb 2022 12:53:59 GMT
content-type: text/plain; charset=utf-8
content-length: 9

The Strict-Transport-Security header is clearly missing in the server response.

A review of the add_header documentation shows why:

Adds the specified field to a response header provided that the response code equals 200, 201 (1.3.10), 204, 206, 301, 302, 303, 304, 307 (1.1.16, 1.0.13), or 308 (1.13.0).

As the response code was 404, the additional header is ignored. To enforce the header anyway, regardless of the response code, use the additional 'always' string:

server {
  listen 443 ssl http2;
  server_name api.example.com;
  access_log /var/log/nginx/api.example.com.access.log;
  error_log /var/log/nginx/api.example.com.error.log;

[... ssl settings ... ]

  add_header Strict-Transport-Security max-age=2678400 always;

  location / {
    proxy_pass http://127.0.0.1:8080;
  }

}

The additional header now shows up, even on a 404 response:

ck@mint ~ $ curl -I https://api.example.com
HTTP/2 404
server: nginx
date: Tue, 08 Feb 2022 13:13:17 GMT
content-type: text/plain; charset=utf-8
content-length: 9
strict-transport-security: max-age=2678400

Multiple add_headers used in different contexts

Another problem can show up, when add_header is defined several times in different contexts.

In the following example, add_header is used once in the server and once in the location context:

server {
  listen 443 ssl http2;
  server_name api.example.com;
  access_log /var/log/nginx/api.example.com.access.log;
  error_log /var/log/nginx/api.example.com.error.log;

[... ssl settings ... ]

  add_header Strict-Transport-Security max-age=2678400 always;

  location / {
    add_header X-Debug true always;
    proxy_pass http://127.0.0.1:8080;
  }

}

But a http request to / now only shows the X-Debug header in the response:

ck@mint ~ $ curl -I https://api.example.com/
HTTP/2 404
server: nginx
date: Tue, 08 Feb 2022 13:48:32 GMT
content-type: text/plain; charset=utf-8
content-length: 9
x-debug: true

Where did the Strict-Transport-Security header go?!

The documentation mentions the following:

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

But frankly, the meaning of it is quite difficult to understand. A much better explanation can be found on Peter Bengtsson's blog post:

When you use add_header in a location block in Nginx, it undoes all "parent" add_header directives

The easiest fix is to move all add_header lines to the server context. They will then be applied across all locations within this server context.

The more complex (and rather annoying) solution would be to define all the additional headers in each location.

Note (to self): You already solved that Nginx missing response header problem in the past (2017), d'uh!


Add a comment

Show form to leave a comment

Comments (newest first)

No comments yet.

RSS feed

Blog Tags:

  AWS   Android   Ansible   Apache   Apple   Atlassian   BSD   Backup   Bash   Bluecoat   CMS   Chef   Cloud   Coding   Consul   Containers   CouchDB   DB   DNS   Database   Databases   Docker   ELK   Elasticsearch   Filebeat   FreeBSD   Galera   Git   GlusterFS   Grafana   Graphics   HAProxy   HTML   Hacks   Hardware   Icinga   Influx   Internet   Java   KVM   Kibana   Kodi   Kubernetes   LVM   LXC   Linux   Logstash   Mac   Macintosh   Mail   MariaDB   Minio   MongoDB   Monitoring   Multimedia   MySQL   NFS   Nagios   Network   Nginx   OSSEC   OTRS   Office   OpenSearch   PGSQL   PHP   Perl   Personal   PostgreSQL   Postgres   PowerDNS   Proxmox   Proxy   Python   Rancher   Rant   Redis   Roundcube   SSL   Samba   Seafile   Security   Shell   SmartOS   Solaris   Surveillance   Systemd   TLS   Tomcat   Ubuntu   Unix   VMWare   VMware   Varnish   Virtualization   Windows   Wireless   Wordpress   Wyse   ZFS   Zoneminder