LEARN · DEBUGGING GUIDE

ASP.NET Core CORS Policy Not Working: Debugging Preflight and Header Mismatches

CORS errors in ASP.NET Core usually come from misconfigured middleware, missing headers, or preflight mismatches. This guide walks through the actual debugging steps that work in production.

IntermediateDotnet7 min read

What this usually means

ASP.NET Core's CORS middleware is either not registered, not invoked in the right order, or the policy doesn't match the request's origin and headers. The most common trap is that CORS middleware must be called before UseRouting and UseEndpoints, but after UseCors is placed in the pipeline. Also, the preflight OPTIONS request must be handled correctly, and if you have custom middleware that short-circuits the pipeline (like authentication or custom response headers), it can block CORS headers from being added. Another frequent cause is that the CORS policy specifies allowed origins, methods, or headers that don't exactly match what the client sends.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Run `curl -X OPTIONS -H "Origin: https://yourclient.com" -H "Access-Control-Request-Method: GET" -H "Access-Control-Request-Headers: authorization" https://yourapi.com/api/endpoint -v` and inspect response headers for Access-Control-Allow-Origin
  • 2Check the order of middleware in Startup.cs: ensure app.UseCors() is placed before app.UseRouting() and app.UseEndpoints()
  • 3If using multiple policies, verify the policy name in the [EnableCors] attribute matches exactly (case-sensitive)
  • 4Look for any custom middleware that sets response headers or calls context.Response.WriteAsync() before CORS middleware runs
  • 5Check if the API returns a 200 OK or 204 No Content for OPTIONS requests — if not, the server may be rejecting preflight
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchStartup.cs (or Program.cs) — order of app.UseCors(), app.UseRouting(), app.UseEndpoints()
  • searchCORS policy configuration — AddCors() options like WithOrigins(), AllowAnyHeader(), AllowAnyMethod()
  • searchController or action method — [EnableCors("PolicyName")] attribute
  • searchWeb.config or IIS settings — if hosted on IIS, check for custom headers or OPTIONSVerbHandler
  • searchNetwork tab in browser DevTools — examine request/response headers for OPTIONS and actual request
  • searchApplication logs — add logging in CORS middleware or use built-in CORS logging (set LogLevel.Information for Microsoft.AspNetCore.Cors.Infrastructure)
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningCORS middleware placed after UseRouting() — so it never runs for matched endpoints
  • warningPreflight OPTIONS request receives a 405 Method Not Allowed because the server doesn't handle OPTIONS
  • warningPolicy allows origin http://example.com but client sends https://example.com (scheme mismatch)
  • warningAllowCredentials() is used with AllowAnyOrigin() which is invalid — must specify exact origins
  • warningCustom middleware sets response status code or writes to response before CORS middleware runs
  • warningReverse proxy (nginx, IIS ARR) strips or modifies CORS headers before they reach the client
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildMove app.UseCors() to immediately after app.UseRouting() and before app.UseEndpoints()
  • buildExplicitly allow OPTIONS verb in your endpoint routing or add a global OPTIONS handler
  • buildUse SetIsOriginAllowed to dynamically validate origins with custom logic (e.g., regex)
  • buildWhen using AllowCredentials(), replace AllowAnyOrigin() with specific origins using WithOrigins()
  • buildIf behind a reverse proxy, configure the proxy to pass through CORS headers or add CORS at the proxy level
  • buildAdd a catch-all CORS policy in middleware to ensure headers are set even on error responses
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedSend a preflight OPTIONS request with curl and confirm the Access-Control-Allow-Origin header matches your client origin
  • verifiedOpen browser DevTools, clear cache, and make an actual cross-origin request — check that the response includes the expected CORS headers
  • verifiedTest with a tool like Postman or Insomnia by setting Origin header and checking response headers
  • verifiedCheck server logs for CORS middleware messages (e.g., 'CORS policy execution succeeded')
  • verifiedDeploy to a staging environment that mirrors production to verify the fix works end-to-end
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDon't place app.UseCors() after app.UseEndpoints() — it will never run for matched routes
  • warningDon't use AllowAnyOrigin() with AllowCredentials() — it throws an exception at runtime
  • warningDon't forget that preflight requests use OPTIONS method and must be handled before authorization
  • warningDon't assume localhost works the same as production — browsers treat localhost as a secure context differently
  • warningDon't rely solely on browser caching — always hard refresh (Ctrl+Shift+R) or use incognito mode
( 07 )War story

The Staging Environment CORS Mystery

Senior Backend EngineerASP.NET Core 6.0, React SPA hosted on Azure Static Web Apps, API on Azure App Service

Timeline

  1. 09:15Deploy new API endpoint for user profile update
  2. 09:30QA reports CORS error in staging: 'Access to fetch at ... from origin ... has been blocked by CORS policy'
  3. 09:45Check browser DevTools: preflight OPTIONS returns 204 but no Access-Control-Allow-Origin header
  4. 10:00Run curl OPTIONS locally — works fine, headers present. Confirm issue only on staging
  5. 10:15Compare staging and local Startup.cs — identical. Notice staging uses Azure AD authentication middleware
  6. 10:30Check Azure App Service logs: preflight request hits middleware pipeline but Authentication middleware returns 401 before CORS can add headers
  7. 10:45Identify that authentication middleware runs before CORS in the pipeline, causing preflight to fail
  8. 11:00Move app.UseCors() before app.UseAuthentication() in Configure()
  9. 11:15Redeploy and confirm preflight now returns proper CORS headers. QA passes.

We had a perfectly working CORS policy in development. The moment we deployed to staging, the React app started throwing CORS errors. The browser console showed a generic 'Access-Control-Allow-Origin header missing' on the preflight request. I was confident the policy was correct because it worked locally.

After spending 30 minutes comparing configurations, I decided to check the actual HTTP response of the OPTIONS request using curl. Locally I got the expected headers; on staging, the preflight returned 204 No Content with no CORS headers. That's when I suspected middleware ordering.

I checked the pipeline: app.UseCors() was after app.UseAuthentication(). The Azure AD middleware was rejecting the preflight request (no bearer token) before CORS could add headers. The fix was simple: move UseCors before UseAuthentication. After redeploy, the preflight succeeded and the actual requests worked. The lesson: CORS must run early in the pipeline, especially when authentication is involved.

Root cause

Authentication middleware placed before CORS middleware in the pipeline, causing preflight requests to be rejected before CORS headers were added.

The fix

Moved app.UseCors() before app.UseAuthentication() in the Configure method.

The lesson

Always put CORS middleware early in the pipeline, before any middleware that might reject requests (authentication, custom authorization, etc.).

( 08 )How ASP.NET Core CORS Middleware Works

The CORS middleware in ASP.NET Core is responsible for handling cross-origin requests by adding the appropriate Access-Control-* headers. It intercepts both preflight OPTIONS requests and actual requests (GET, POST, etc.).

The middleware is configured via `AddCors()` which defines policies, and `UseCors()` which applies a policy. The order in the pipeline is critical: `UseCors()` must be called before `UseRouting()` or at least before `UseEndpoints()` because endpoints are matched after routing. If `UseCors()` is after `UseEndpoints()`, the middleware will never execute for requests that match an endpoint.

When a preflight request arrives, the middleware checks if the origin, methods, and headers are allowed. If they are, it sets the response headers and typically returns 204 No Content. If not, it omits the headers, causing the browser to block the actual request.

( 09 )Common Preflight Pitfalls and How to Debug Them

The most common issue is that the preflight request fails because the server doesn't handle OPTIONS requests. ASP.NET Core's endpoint routing may return 405 if you haven't explicitly allowed OPTIONS. To fix, either add `app.UseCors()` before routing or configure your endpoints to accept OPTIONS: `endpoints.MapControllers().RequireCors("policyName");`.

Another pitfall is using `AllowAnyOrigin()` with `AllowCredentials()`. This combination throws an invalid operation exception because browsers block credentials when the origin is wildcard. You must specify exact origins with `WithOrigins()`. Additionally, if you need dynamic origins, use `SetIsOriginAllowed()`.

Debugging tip: enable CORS logging by setting `LogLevel.Information` for `Microsoft.AspNetCore.Cors.Infrastructure` in your `appsettings.json`. Then inspect the logs to see if the policy was matched and which headers were added.

( 10 )Impact of Reverse Proxies and Load Balancers

When your ASP.NET Core API sits behind a reverse proxy like nginx, IIS ARR, or Azure Front Door, the proxy may strip or modify CORS headers. For example, nginx by default does not forward the `Origin` header, which causes the CORS middleware to see a null origin.

To fix, configure the proxy to pass through the `Origin` header and preserve the CORS response headers. In nginx, add `proxy_set_header Origin $http_origin;` and ensure `proxy_pass` headers are not overridden.

Also, if the proxy handles SSL termination, the scheme in the origin may change (http vs https). Your CORS policy must match the origin exactly as sent by the client, which includes the scheme.

( 11 )Advanced Scenarios: Multiple Policies and Dynamic Origins

If your application needs different CORS policies for different endpoints (e.g., public API vs authenticated API), define multiple policies in `AddCors()` and apply them using the `[EnableCors("PolicyName")]` attribute on controllers or actions.

For dynamic origins (e.g., allowing a list of domains from a database), use `SetIsOriginAllowed((origin) => { return allowedOrigins.Contains(origin); })` in the policy options. This gives you full control over origin validation.

Be careful with performance: if you have many origins, caching the allowed origins can reduce database calls. Also, note that `SetIsOriginAllowed` runs for every request, so keep the logic efficient.

( 12 )Testing and Verifying CORS Configuration

The most reliable way to test CORS is using curl for preflight requests: `curl -X OPTIONS -H "Origin: https://client.com" -H "Access-Control-Request-Method: GET" -H "Access-Control-Request-Headers: authorization" https://api.com/endpoint -v`

Check the response headers: `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers`, `Access-Control-Allow-Credentials`. All should be present and match the client's request.

For actual requests, use curl with the `-H "Origin: https://client.com"` header and verify the response also includes the CORS headers. In browser, use the Network tab and ensure the preflight and actual request both have the headers.

Frequently asked questions

Why does my CORS policy work locally but fail when deployed?

Common reasons: different middleware order (e.g., authentication middleware added in staging), reverse proxy stripping headers, or the deployed URL includes a different scheme (http vs https) or port. Compare local and deployed configurations carefully.

Do I need to handle OPTIONS requests manually?

No, the CORS middleware handles OPTIONS requests automatically if placed correctly. However, if you have custom middleware that short-circuits, you may need to ensure OPTIONS passes through. Also, if you use endpoint routing without a catch-all, OPTIONS might return 405 unless you explicitly allow it.

What does 'Access-Control-Allow-Origin' header missing mean?

It means the server did not include the header in the response. The browser requires this header to allow cross-origin requests. Check if your CORS middleware executed (log it), and verify the origin you requested is in the allowed origins list.

Can I use AllowAnyOrigin with AllowCredentials?

No. This combination is invalid and will throw an exception. The browser specification prohibits wildcard origins with credentials. You must specify exact origins with WithOrigins() when using AllowCredentials().

How do I debug CORS in a production environment?

Enable CORS logging in your appsettings.json: set LogLevel.Information for Microsoft.AspNetCore.Cors.Infrastructure. Use curl to simulate preflight requests from different origins. Check your reverse proxy logs to ensure headers are forwarded. Also, use browser DevTools with 'Disable cache' enabled.