How to Fix "Nginx proxy_pass Not Forwarding Custom Headers"
Quick answer
Your backend expects a custom header β an API key, a request ID, an Authorization token β but it never arrives, even though the client is clearly sending it...
Your backend expects a custom header β an API key, a request ID, an Authorization token β but it never arrives, even though the client is clearly sending it and Nginx is proxying the request. Nginx doesn't forward every header by default the way you might assume, and a few specific ones are stripped or altered unless you explicitly configure otherwise.
The Problem
The client sends a header, but the backend logs show it never arrived:
$ curl -H "X-Request-Id: abc123" https://yourapp.com/api/data
Backend logging middleware shows the header simply isn't present in the request it received:
[app] Incoming headers: {"host":"127.0.0.1:3000","user-agent":"curl/8.4.0"}
# X-Request-Id is missing entirely
This is especially common (and confusing) with the Authorization header specifically, which has special handling in some Nginx configurations.
Why It Happens
By default, Nginx forwards most headers through a proxied request, but there are specific, well-documented exceptions and gotchas that trip people up:
- Underscore-containing headers are dropped by default β Nginx's
underscores_in_headersdirective defaults tooff, silently discarding any incoming header with an underscore in its name (likeX_Request_Id), which is a subtle trap since dashes work fine but underscores don't. - The
Authorizationheader can be stripped by certain configurations β some setups explicitly clear it (a common but often unintentional pattern copied from older tutorials dealing with basic auth), or a precedinglocationblock'sproxy_passdoesn't propagate it correctly. - Missing explicit
proxy_set_headerdirectives for headers you're adding or modifying yourself, rather than just passing through β Nginx doesn't automatically know to add custom application-specific headers unless told to. - A header set earlier in the config gets silently overridden by a more specific, later
locationblock redefining the same header without realizing it's replacing rather than adding to the earlier definition.
The Fix
For the underscore issue specifically, either rename the header to use dashes (the more portable, HTTP-conventional choice) or explicitly enable underscore support:
server {
underscores_in_headers on;
}
Renaming to dashes is generally the more robust fix, since not every proxy or CDN in front of Nginx will respect underscores_in_headers consistently:
# Prefer this
X-Request-Id: abc123
# Over this
X_Request_Id: abc123
To explicitly forward a header (or ensure it survives, even if it should be passed through by default), use proxy_set_header directly in the relevant location block:
location /api/ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header X-Request-Id $http_x_request_id;
proxy_set_header Authorization $http_authorization;
}
$http_<header_name> (lowercased, underscores instead of dashes) is Nginx's variable syntax for reading an incoming request header, which you can then explicitly forward, modify, or use to construct a new header value.
If you need to add a header that doesn't come from the client at all β for example, injecting a static API key toward your backend β set it directly with a literal value instead:
location /api/ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header X-Internal-Key "your-static-key-here";
}
Always include the standard forwarding headers too, since these are commonly needed by backends for logging and security purposes and aren't set automatically:
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
Reload Nginx to apply the changes:
sudo nginx -t
sudo systemctl reload nginx
Still Not Working?
If a header still isn't reaching the backend after explicitly setting it, check whether multiple nested location blocks are involved and one of them is redefining the same header set without the value you expect β Nginx's header inheritance across blocks isn't always intuitive, since defining any proxy_set_header in a more specific block can reset the full set of inherited headers from a parent block rather than just adding to them:
nginx -T | grep -A 5 "location /api/"
nginx -T dumps the fully resolved configuration, including any included files, which makes it much easier to spot exactly which block is actually setting (or failing to set) a given header for the specific request path you're debugging.