When working with CORS headers in NGINX, you might encounter this common scenario:
add_header Access-Control-Allow-Origin http://dev.anuary.com;
add_header Access-Control-Allow-Origin https://dev.anuary.com;
Instead of sending separate headers, NGINX combines them into:
Access-Control-Allow-Origin: http://dev.anuary.com, https://dev.anuary.com
NGINX's behavior follows the HTTP specification where multiple headers with the same name get merged with comma separation. This is problematic for CORS because:
- The CORS specification requires exact origin matching
- Multiple values in Access-Control-Allow-Origin are not allowed
- Browsers will reject comma-separated origins
The proper way to handle multiple possible origins is to evaluate the incoming Origin header:
map $http_origin $cors_origin {
default "";
"~^https?://(dev\.anuary\.com|api\.anuary\.com)$" $http_origin;
}
server {
...
add_header Access-Control-Allow-Origin $cors_origin;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type";
add_header Access-Control-Allow-Credentials "true";
...
}
For more complex origin matching:
map $http_origin $cors_origin {
default "";
"~^https?://([a-z0-9-]+\.)?anuary\.com(:[0-9]+)?$" $http_origin;
"~^https?://anuary-(dev|stage|test)\.example\.com$" $http_origin;
}
Don't forget to properly handle OPTIONS requests:
location / {
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin $cors_origin;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'Authorization,Content-Type';
add_header Access-Control-Max-Age 1728000;
add_header Content-Type 'text/plain; charset=utf-8';
add_header Content-Length 0;
return 204;
}
}
After implementing these changes, verify with:
curl -H "Origin: http://dev.anuary.com" -I http://yourdomain.com/api
curl -H "Origin: https://api.anuary.com" -I http://yourdomain.com/api
When working with NGINX's add_header
directive, many developers encounter an unexpected behavior where multiple declarations of the same header get concatenated into a single header with comma-separated values. For CORS headers in particular, this can cause issues with browser security enforcement.
Consider this common CORS setup attempt:
add_header Access-Control-Allow-Origin http://dev.example.com;
add_header Access-Control-Allow-Origin https://dev.example.com;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
NGINX will output:
Access-Control-Allow-Origin: http://dev.example.com, https://dev.example.com
Access-Control-Allow-Methods: GET,POST,OPTIONS
Most browsers will reject such CORS responses because the spec requires a single origin or the wildcard *
, not multiple values.
The most robust solution is to use NGINX's map
directive to select the appropriate header value:
map $http_origin $cors_origin {
default "";
"~^https?://dev\.example\.com" $http_origin;
"~^https?://staging\.example\.com" $http_origin;
}
server {
...
add_header Access-Control-Allow-Origin $cors_origin;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Credentials "true";
...
}
For simpler cases, you can use conditional logic:
set $cors "";
if ($http_origin ~* ^https?://(dev|staging)\.example\.com$) {
set $cors $http_origin;
}
add_header Access-Control-Allow-Origin $cors;
Remember that add_header
directives inherit within the same context level but don't merge between different levels. If you define headers in both server and location blocks, only the location block headers will be sent.
After making changes, verify your headers with curl:
curl -I -H "Origin: http://dev.example.com" https://yourdomain.com/api