When implementing CORS for multiple subdomains, many developers make the mistake of comma-separating origins in the Access-Control-Allow-Origin
header. The CORS specification explicitly states that this header can only contain one origin or the wildcard *
, but never multiple origins.
Here's the corrected Nginx configuration that dynamically handles multiple allowed origins:
upstream app_server {
server unix:/tmp/api.example.com.sock fail_timeout=0;
}
server {
listen 80;
server_name api.example.com;
# Check if the Origin header matches allowed domains
set $cors "";
if ($http_origin ~* (https?://(example\.com|developers\.example\.com))) {
set $cors $http_origin;
}
location / {
if ($cors != "") {
add_header 'Access-Control-Allow-Origin' "$cors";
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'Content-Type,Accept';
}
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://app_server;
}
}
For OPTIONS requests (preflight), you'll need to add specific handling:
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' "$cors";
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'Content-Type,Accept';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
# Rest of your proxy configuration...
}
Here's how to properly test both implementations:
// jQuery example with credentials
$.ajax("http://api.example.com/endpoint", {
xhrFields: {
withCredentials: true
},
headers: {
'Content-Type': 'application/json'
}
});
// Fetch API example
fetch("http://api.example.com/endpoint", {
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
If you're still seeing CORS errors in Chrome despite correct headers:
- Clear cache and hard reload (Ctrl+Shift+R)
- Disable all Chrome extensions temporarily
- Check for typos in domain names (trailing slashes matter!)
- Verify HTTPS/HTTP protocol consistency
For more complex origin patterns:
if ($http_origin ~* ^https?://(.*\.)?example\.com(:[0-9]+)?$) {
set $cors $http_origin;
}
When implementing CORS for a multi-domain setup, a common mistake is trying to list multiple domains in the Access-Control-Allow-Origin
header. The HTTP specification explicitly states this header can only contain one origin or *
, not multiple comma-separated values.
Here's the corrected nginx configuration that dynamically checks and allows specific domains:
server {
listen 80;
server_name api.example.com;
# Dynamic CORS handling
set $cors "";
if ($http_origin ~* (https?://(example\.com|developers\.example\.com))) {
set $cors $http_origin;
}
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$cors';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type,Accept,Authorization';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
add_header 'Access-Control-Allow-Origin' '$cors';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Expose-Headers' 'Authorization';
proxy_pass http://app_server;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
The corresponding jQuery AJAX call should include credentials when needed:
$.ajax({
url: "http://api.example.com/endpoint",
type: 'GET',
dataType: 'json',
xhrFields: {
withCredentials: true
},
crossDomain: true,
success: function(response) {
console.log(response);
},
error: function(xhr, status) {
console.error(status);
}
});
- Wildcard conflicts: Using
*
withAccess-Control-Allow-Credentials: true
will fail - Header order: OPTIONS responses need proper header sequencing
- Protocol matching:
http://
andhttps://
are treated as different origins
For more complex requirements, use map blocks in nginx:
map $http_origin $allow_origin {
default "";
"~^https://(sub\.example\.com|example\.com)$" $http_origin;
"~^http://(localhost|dev\.example\.com):[0-9]+$" $http_origin;
}
server {
# ... other config ...
add_header 'Access-Control-Allow-Origin' $allow_origin;
}