No 'Access-Control-Allow-Origin' Header Is Present in ASP.NET Core: Causes and Fixes

The browser console message is always some version of this:
Access to fetch at 'https://api.example.com/orders' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
Getting "No 'Access-Control-Allow-Origin' header is present" from an ASP.NET Core API is one of those errors where the obvious fix - add CORS - is usually already done. The request works in Postman, it works in curl, the policy is right there in Program.cs, and the browser still refuses it. In production I've debugged this a dozen times, and the cause was rarely a missing policy. It was pipeline order, a trailing slash, or an exception that stripped the headers on the way out.
The diagnostic path below goes in the order that finds the cause fastest. The complete working configuration, including credentialed cross-origin auth and the middleware ordering tests that keep it correct, is on Patreon.
What the Error Actually Means
CORS is enforced entirely by the browser, not by your server. Your API returned a perfectly valid response; the browser looked for an Access-Control-Allow-Origin header that permits the calling page's origin, did not find one, and refused to hand the response to JavaScript.
Two consequences follow, and both matter for debugging:
Postman and curl will never reproduce this. They do not enforce CORS. A working curl proves nothing about the browser path.
The server usually thinks it succeeded. Your logs show a 200. The failure exists only in the browser.
So the real question is never "why did the request fail" - it is "why did the response come back without that header".
Cause 1: Middleware Order
The most common cause by a wide margin. UseCors has to sit after UseRouting and before UseAuthorization and your endpoint mapping:
app.UseRouting();
app.UseCors("Frontend"); // after routing, before auth
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
Put UseCors after UseAuthorization and the CORS headers are added too late for rejected requests. Put it before UseRouting and endpoint-specific policies never resolve. Either way the symptom is identical to having no CORS at all. Our middleware pipeline checklist for .NET teams covers the wider ordering rules that this one belongs to.
Also check that nothing short-circuits ahead of UseCors. A custom middleware that returns early - a maintenance-mode gate, an API key check, a legacy rewrite - produces a response that never reaches the CORS middleware.
Cause 2: The Origin Does Not Match Exactly
Origins are compared as exact strings across scheme, host, and port. These are all different origins, and only the exact one works:
| Configured | Browser sends | Match? |
|---|---|---|
https://app.example.com |
https://app.example.com |
Yes |
https://app.example.com/ |
https://app.example.com |
No - trailing slash |
http://app.example.com |
https://app.example.com |
No - scheme |
https://app.example.com |
https://app.example.com:8443 |
No - port |
https://example.com |
https://app.example.com |
No - subdomain |
The trailing slash is the one that costs people hours, because the string looks correct at a glance. WithOrigins("https://app.example.com") - no slash, ever.
For local development, remember that http://localhost:3000 and http://127.0.0.1:3000 are distinct origins even though they reach the same server.
Cause 3: AllowAnyOrigin Combined With Credentials
If your frontend sends cookies or an Authorization header with credentials: 'include', this configuration cannot work:
// Invalid combination - wildcard origin with credentials
policy.AllowAnyOrigin().AllowAnyHeader().AllowCredentials();
The CORS specification forbids returning Access-Control-Allow-Origin: * alongside Access-Control-Allow-Credentials: true, and ASP.NET Core will not let you configure it. The fix is to name the origins explicitly, or to compute them:
policy.WithOrigins("https://app.example.com")
.AllowAnyHeader()
.AllowCredentials();
When origins are dynamic - per-tenant subdomains, preview deployments - use SetIsOriginAllowed with a predicate that validates against an allow-list. Do not write a predicate that returns true unconditionally; that is AllowAnyOrigin wearing a disguise, and it re-enables the credentialed cross-origin requests the specification was protecting you from.
Cause 4: The Preflight Request Is Being Rejected
Any request that is not a simple GET or POST with a basic content type triggers a preflight: the browser sends OPTIONS first and only sends the real request if that succeeds. When preflight fails, the console reports the same missing-header message, but the actual failure happened on a request you never wrote.
Three things break preflight:
Authentication runs first and returns 401. Preflight requests carry no credentials by design. They must be allowed anonymously, which is what correct middleware ordering gives you.
A custom header is not in the policy. If the client sends
X-Correlation-Idand the policy does not list it, preflight fails.AllowAnyHeader()during development, an explicitWithHeaders(...)list in production.The method is not allowed. A
PATCHorDELETEagainst a policy configured only for GET and POST fails preflight, which frequently gets misreported as a routing problem. Our guide to 405 Method Not Allowed in ASP.NET Core covers where the two overlap.
Check the Network tab for the OPTIONS request specifically. If it is missing or non-2xx, that is your answer.
Cause 5: The Response Is Actually a 500
This is the trap that wastes the most time, and it is the one most CORS write-ups skip entirely.
When your API throws, the exception handling middleware produces the error response. If that middleware sits before UseCors in the pipeline - which it does, because exception handling belongs first - the error response goes out without CORS headers. The browser then reports a CORS failure, and the front-end team spends a day on CORS configuration while the actual bug is an unhandled NullReferenceException in a repository.
How to tell them apart: look at the status code in the Network tab rather than the console message. A 500 with a CORS error in the console is a 500, not a CORS problem. Fix the exception; the CORS error disappears with it.
The same applies to 404s from a mistyped route and 413s from an oversized body. If the console says CORS but the status is anything other than 200, chase the status.
Cause 6: A Proxy Is Stripping or Duplicating the Header
In front of a reverse proxy, an ingress controller, or an API gateway, two things go wrong:
The header is stripped. Some proxy configurations filter response headers they do not recognise.
The header is added twice. If nginx adds
Access-Control-Allow-Originand your app adds it as well, the browser sees two values and rejects the response. The message is identical to having none.
Curl the endpoint from outside the proxy and count the headers:
curl -i -H "Origin: https://app.example.com" https://api.example.com/orders
Exactly one Access-Control-Allow-Origin line, exactly matching the origin you sent. Pick one layer to own CORS and remove it from the other.
Why Can the Browser Not Read My Custom Response Header?
Because Access-Control-Allow-Origin only governs whether the response body is readable. Response headers stay hidden from JavaScript unless you list them explicitly:
policy.WithOrigins("https://app.example.com")
.WithExposedHeaders("X-Total-Count", "X-Correlation-Id");
This shows up as pagination counts or correlation ids being null on the client while clearly present in the Network tab. Microsoft's CORS documentation covers the full set of policy options.
How to Stop It Recurring
Own CORS in exactly one layer. Application or proxy, never both.
Keep origins in configuration, not in code. Environment-specific origin lists that require a rebuild guarantee someone hardcodes a wildcard during an incident.
Never ship
AllowAnyOriginto production. It is a fine local default and a standing invitation in production. Our CORS policy decision guide covers named versus default versus endpoint-level policies properly.Add one automated check that asserts the header. A test that sends an
Originheader and asserts the response carries a matchingAccess-Control-Allow-Origincatches every ordering regression, and ordering regressions are the ones that keep happening.Check the status code before you touch CORS config. Make it the first question anyone asks.
FAQ
Why does my API work in Postman but fail with a CORS error in the browser?
Because CORS is enforced by browsers, not servers. Postman and curl issue the request and hand you the response regardless of CORS headers, so they cannot reproduce the failure. A successful curl only tells you the endpoint works; it says nothing about whether the browser will be permitted to read the response.
Why does the CORS error appear even though I called UseCors?
Almost always ordering. UseCors must come after UseRouting and before UseAuthentication, UseAuthorization, and endpoint mapping. It also has to run before any middleware that short-circuits the pipeline, since a response produced upstream never reaches the CORS middleware to have headers added.
How do I allow credentials with CORS in ASP.NET Core?
Name the origins explicitly with WithOrigins(...) and add AllowCredentials(). The specification forbids combining credentials with a wildcard origin, so AllowAnyOrigin() is not an option here. For dynamic origins such as per-tenant subdomains, use SetIsOriginAllowed with a predicate that checks a real allow-list rather than returning true.
Why do I get a CORS error only for PUT, PATCH, or DELETE requests?
Those methods trigger a preflight OPTIONS request, and GET or POST with a simple content type often does not. So the policy gaps that only affect preflight - missing methods, unlisted custom headers, authentication running before CORS - surface exclusively on those verbs. Inspect the OPTIONS request in the Network tab rather than the request you wrote.
Is a CORS error in the console always a CORS problem?
No, and this is the most valuable thing to internalise. Any error response produced before the CORS middleware runs, including 500s from exception handling and 404s from routing, goes out without CORS headers and is reported by the browser as a CORS failure. Read the HTTP status first: anything other than a success status means you are chasing the wrong bug.
Should CORS be handled in ASP.NET Core or at the API gateway?
Either works, but only one at a time. Two layers both adding Access-Control-Allow-Origin produce a duplicated header that browsers reject with the same message as having none. Handling it in the application keeps the policy versioned with the code; handling it at the gateway centralises it across services. Pick one and remove the other.
About the Author
I'm Celin Daniel, Co-founder of Coding Droplets. I've been building .NET and ASP.NET Core systems in production for 13+ years - APIs, distributed backends, enterprise platforms. Everything I write here comes from real shipping experience: patterns that held up, trade-offs that bit us, and lessons learned the hard way.
GitHub: codingdroplets
YouTube: Coding Droplets
Website: codingdroplets.com





