CORS
The React storefront runs on https://shop.example.com and calls the API on https://api.shop.example.com — a different origin. Browsers block that unless the API says it is allowed. CORS (Cross-Origin Resource Sharing) is how the API says so.
On this page · 6 min · Intermediate
On this page
- In simple words
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 29, 2026
Related tutorials
In simple words#
CORS is the building’s guest list for browsers: only websites (origins) on the list may call your API from JavaScript.
- Origin
- Scheme + domain + port of a site.
- CORS policy
- Which origins, methods and headers are allowed.
- Browser-only
- Servers and Postman ignore CORS.
Learning objectives#
- Explain the same-origin policy and preflight requests
- Configure a named CORS policy with origins, methods and headers
- Allow credentials and expose custom headers safely
- Place UseCors correctly in the pipeline
Explanation#
Browsers only let a page read responses from other origins (scheme + host + port) if the server answers with Access-Control-Allow-Origin for that origin. Non-browser clients (mobile apps, curl, servers) ignore CORS entirely — it is not an access-control mechanism for your data.
Non-simple requests (PUT/DELETE, JSON bodies, Authorization headers) are preceded by a preflight OPTIONS request. The API answers with the allowed methods and headers, and Access-Control-Max-Age lets the browser cache that answer.
Configure named policies: AddCors(o => o.AddPolicy("Frontend", p => p.WithOrigins("https://shop.example.com", "http://localhost:5173").AllowAnyHeader().WithMethods("GET","POST","PUT","DELETE").AllowCredentials())).
AllowCredentials (for cookies) cannot be combined with AllowAnyOrigin. List exact origins, ideally from configuration per environment. WithExposedHeaders("X-Total-Count") lets JavaScript read custom response headers.
Call app.UseCors("Frontend") after UseRouting and before UseAuthentication/UseAuthorization (or use [EnableCors] / RequireCors per endpoint).
Syntax#
builder.Services.AddCors(o => o.AddPolicy("Frontend", p => p
.WithOrigins("https://shop.example.com", "http://localhost:5173")
.WithMethods("GET", "POST", "PUT", "DELETE")
.AllowAnyHeader()
.WithExposedHeaders("X-Total-Count")
.SetPreflightMaxAge(TimeSpan.FromMinutes(10))));
app.UseCors("Frontend");
Code example: A front-end CORS policy and a preflight#
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders(); // keep the demo output clean
builder.Services.AddControllers();
builder.Services.AddCors(options => options.AddPolicy("Frontend", policy => policy
.WithOrigins("https://shop.example.com", "http://localhost:5173")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization")
.WithExposedHeaders("X-Total-Count")
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromMinutes(10))));
var app = builder.Build();
app.UseRouting();
app.UseCors("Frontend");
app.MapControllers();
app.Urls.Add("http://127.0.0.1:5225");
await app.StartAsync();
using var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:5225") };
string Header(HttpResponseMessage r, string name) =>
r.Headers.TryGetValues(name, out var v) ? string.Join(",", v) : "-";
foreach (var origin in new[] { "http://localhost:5173", "https://evil.example" })
{
var request = new HttpRequestMessage(HttpMethod.Get, "/api/products");
request.Headers.Add("Origin", origin);
var response = await http.SendAsync(request);
Console.WriteLine(quot;GET from {origin,-22} {(int)response.StatusCode} allow-origin={Header(response, "Access-Control-Allow-Origin")} " +
quot;credentials={Header(response, "Access-Control-Allow-Credentials")} expose={Header(response, "Access-Control-Expose-Headers")}");
}
var preflight = new HttpRequestMessage(HttpMethod.Options, "/api/products/1");
preflight.Headers.Add("Origin", "https://shop.example.com");
preflight.Headers.Add("Access-Control-Request-Method", "PUT");
preflight.Headers.Add("Access-Control-Request-Headers", "content-type,authorization");
var answer = await http.SendAsync(preflight);
Console.WriteLine(quot;OPTIONS preflight for PUT {(int)answer.StatusCode} methods={Header(answer, "Access-Control-Allow-Methods")} " +
quot;headers={Header(answer, "Access-Control-Allow-Headers")} max-age={Header(answer, "Access-Control-Max-Age")}");
await app.StopAsync();
// Controllers/ProductsController.cs
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
[HttpGet]
public IActionResult GetAll()
{
Response.Headers["X-Total-Count"] = "2";
return Ok(new[] { "Wireless Mouse", "USB-C Hub" });
}
[HttpPut("{id:int}")]
public IActionResult Update(int id) => NoContent();
}
GET from http://localhost:5173 200 allow-origin=http://localhost:5173 credentials=true expose=X-Total-Count GET from https://evil.example 200 allow-origin=- credentials=- expose=- OPTIONS preflight for PUT 204 methods=GET,POST,PUT,DELETE headers=Content-Type,Authorization max-age=600
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- A request from the allowed localhost:5173 origin gets Access-Control-Allow-Origin with that origin, credentials allowed and X-Total-Count exposed.
- The same request from evil.example still returns 200 — CORS is enforced by the browser, which will refuse to let that page read the response because no CORS headers were sent.
- The browser’s preflight OPTIONS for a PUT with JSON and Authorization gets 204 with the allowed methods and headers.
- Max-Age = 600 lets the browser skip preflights for 10 minutes.
- UseCors sits after UseRouting so endpoint-specific CORS settings also work.
Real-world use case#
In development the React app runs on localhost:5173; in production on shop.example.com. Both origins come from configuration, so staging and production each allow exactly their own front end.
Common mistakes#
ASP.NET Core refuses this combination; reflecting any origin with credentials would let every site make authenticated calls.
curl, Postman and servers ignore CORS. Authentication and authorization protect data.
After UseAuthorization (or after endpoints), preflights can fail with 401/404 and no CORS headers.
Best practices#
- Exact origins from configuration
- Only the methods and headers you need
- Expose only the custom headers clients read
- Preflight max-age to reduce OPTIONS traffic
Remember this#
Browser enforces, server declares.
Credentials → exact origins, never “*”.
UseRouting → UseCors → UseAuthentication → UseAuthorization.
Practice exercise#
Your task: Read the allowed origins from configuration (Cors:Origins array) instead of hard-coding them.
Need a hint?
Show solution
// appsettings.json
{
"Cors": { "Origins": [ "http://localhost:5173", "https://shop.example.com" ] }
}
// Program.cs
var builder = WebApplication.CreateBuilder(args);
var origins = builder.Configuration.GetSection("Cors:Origins").Get<string[]>() ?? [];
builder.Services.AddCors(options => options.AddPolicy("Frontend", policy => policy
.WithOrigins(origins)
.AllowAnyHeader()
.AllowAnyMethod()));
var app = builder.Build();
app.UseCors("Frontend");
app.Run();
Mini quiz#
Who enforces CORS?
Want more? Practise all ASP.NET Core Web API MCQs · ASP.NET Core Web API interview questions
My notes#
Saved in this browser.