Configuring CORS Correctly
IntermediateAllow the origins you actually deploy, never a wildcard with credentials, and know why the same config behaves differently in production.
Overview
Configuring CORS is three lines of middleware, which is why it is usually configured badly. The wildcard origin gets used because it makes the error go away, and then breaks the moment cookies are involved — because the specification forbids that combination outright. The other recurring surprise is that a config which works locally fails in production, since localhost:3000 was the only origin ever listed. Both are avoidable by treating the allowed-origin list as configuration rather than a constant.
FastAPI and Spring Boot
The real configuration on both stacks, with origins from the environment.
# FastAPI
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS, # from env, never hardcoded
allow_credentials=True, # required for cookies
allow_methods=["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization", "X-Request-Id"],
expose_headers=["X-Request-Id", "X-Total-Count"],
max_age=86400,
)
# ALLOWED_ORIGINS=https://aicancode.org,https://www.aicancode.org,
# https://aicancode-git-preview.vercel.app
// Spring Boot
@Bean
CorsConfigurationSource corsConfigurationSource() {
var c = new CorsConfiguration();
c.setAllowedOrigins(List.of(env.getProperty("app.origins").split(",")));
c.setAllowCredentials(true);
c.setAllowedHeaders(List.of("Content-Type", "Authorization"));
c.setMaxAge(86400L);
var src = new UrlBasedCorsConfigurationSource();
src.registerCorsConfiguration("/**", c);
return src;
}
// With Spring Security, CORS must be enabled in the filter chain too,
// or the security filter rejects the preflight before CORS runs.The Wildcard Trap
Why * silently stops working the moment you send credentials.
// This is invalid, and the browser rejects it:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
// "The value of the 'Access-Control-Allow-Origin' header must not be
// the wildcard '*' when the request's credentials mode is 'include'"
// The reason is obvious once stated: * means "any site may read
// this". Combined with cookies, that is every site on the internet
// reading your users' authenticated responses.
// So with cookies you must echo a specific, validated origin:
origin = request.headers.get("origin")
if origin in settings.ALLOWED_ORIGINS:
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Vary"] = "Origin" # or a CDN caches one
# origin's response for all
// Never reflect the origin unvalidated — that is a wildcard with
// extra steps, and it is a real vulnerability:
response.headers["Access-Control-Allow-Origin"] = origin # NO
// Also careful with regex matching:
r"https://.*\.aicancode\.org" # matches https://evil.com/x.aicancode.org
# depending on the pattern. Prefer an
# explicit list.What Bites in Production
The cases that only appear once there are two real domains.
// 1. Preview deployments. Vercel gives every branch its own URL,
// and none of them are in your allow-list.
ALLOWED_ORIGIN_REGEX = r"^https://aicancode(-[a-z0-9-]+)?\.vercel\.app$"
// Allow previews in staging only. Never widen production for the
// convenience of a preview build.
// 2. www vs apex. https://aicancode.org and https://www.aicancode.org
// are different origins. List both, or redirect one to the other.
// 3. A redirect loses CORS. If /api/problems 301s to
// /api/problems/, the browser follows it but the preflight was
// for the original URL. Match the trailing slash exactly.
// 4. Error responses skip the middleware. A 500 raised before CORS
// runs has no CORS headers, so the browser reports a CORS error
// for what is really a server crash. Always check the server log
// when a CORS error appears out of nowhere.
// 5. A proxy strips headers. Fly, nginx and Cloudflare can all be
// configured to drop or override them. Confirm with:
curl -i -X OPTIONS https://api.aicancode.org/problems \
-H "Origin: https://aicancode.org" \
-H "Access-Control-Request-Method: POST"
// That is the one CORS thing curl IS useful for: inspecting the
// preflight response the server actually sends.
// The alternative that removes CORS entirely: same-origin.
// Proxy /api through the frontend host, and there is no cross-origin
// request to permit. Next.js rewrites do exactly this:
async rewrites() {
return [{ source: '/api/:path*', destination: `${API_URL}/:path*` }]
}
// Cookies then become first-party too, which solves several problems
// at once — at the cost of an extra network hop.Key Points to Remember
- 1Allowed origins belong in configuration, listing every real domain including www and preview URLs
- 2A wildcard origin with credentials is forbidden by the spec — echo a validated origin and set Vary: Origin
- 3Never reflect an unvalidated Origin header; that is a wildcard with extra steps and a real vulnerability
- 4A 500 raised before the CORS middleware produces a CORS error that is really a server crash — check the logs first
- 5Proxying the API through the frontend origin removes CORS entirely and makes cookies first-party
Interview Questions
Sign in to ask AriaWhy can Access-Control-Allow-Origin not be * when credentials are included?
Why is Vary: Origin needed when echoing the origin back?
How can you avoid CORS altogether in a Next.js and FastAPI setup?
Ask Aria about Configuring CORS Correctly
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.