When an ASP.NET Core application runs behind nginx acting as a reverse proxy, duplicate CORS headers may appear in the HTTP response. Browsers reject such responses because the Access-Control-Allow-Origin header must appear at most once.
A typical browser error looks like this:
Access to XMLHttpRequest at 'http://10.0.0.5:8080/api/auth' from origin 'http://localhost:5173'
has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple
values 'http://localhost:5173, *', but only one is allowed.
Inspecting the response headers reveals two separate origins:
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Origin: *
This occurs when both nginx and the ASP.NET Core pipeline attach CORS headers independently. Because the W3C CORS spceification permits only a single value for this header, the browser treats the response as invalid and blocks it.
To resolve the conflict, choose exactly one layer to manage cross-origin policy.
Option 1: Offload CORS to nginx
If the reverse proxy should own the policy, first ensure the upstream application does not emit CORS headers. In ASP.NET Core, simply omit AddCors and UseCors from the service configuration.
Then configure nginx to inject the headers and, critically, hide any headers that might leak from the backand:
upstream api_nodes {
server 127.0.0.1:5000;
}
server {
listen 8080;
location / {
proxy_pass http://api_nodes;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
# Remove CORS headers from the upstream response
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
# Add authoritative CORS headers
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always;
add_header Access-Control-Allow-Credentials true always;
}
}
Using proxy_hide_header guarantees that any accidental CORS configuration in the application layer is stripped before the response reaches the client.
Option 2: Manage CORS in ASP.NET Core
Alternatively, let the application handle CORS and configure nginx to remain transparent. Remove or comment out all add_header directives related to CORS in the nginx site configuration.
Simplified nginx configuration:
server {
listen 8080;
location / {
proxy_pass http://api_nodes;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
# Intentionally no CORS directives here
}
}
In the ASP.NET Core project, register a named policy and apply it early in the middleware pipelien:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy("GatewayPolicy", policy =>
{
policy.WithOrigins("http://localhost:5173", "https://app.example.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
var app = builder.Build();
app.UseCors("GatewayPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Placing UseCors before UseAuthentication and UseAuthorization ensures the headers are written before the request reaches endpoints that might short-circuit the pipeline.