The Complete CORS Error Fix: Diagnosis Table by Console Error Message + Copy-Paste Configs by Server
You've almost certainly been there: the API returns 200 just fine in Postman, but the browser console lights up red. And nine times out of ten you think, "The code is clearly correct—why is this blocked?"
The first misconception to clear up: CORS is not the server rejecting the request—it's the browser intercepting the response. The server sent a perfectly normal response, but if that response lacks the right Access-Control-* headers, the browser says "I can't hand this to you for security reasons" and blocks the result from reaching JavaScript. That's why curl and Postman work while the browser blows up.
This post is structured as error message → cause → server config. Take the English console message as-is, look it up in the diagnosis table, then paste the config for your server stack.
CORS in 30 Seconds
Because of the Same-Origin Policy, browsers block responses from a different origin (protocol + host + port) by default. Calling https://api.com from https://app.com is a different origin (cross-origin), so CORS checks kick in.
Requests fall into two categories.
- Simple request: GET, POST (HEAD), and
Content-Typeoftext/plain,application/x-www-form-urlencoded, ormultipart/form-data. The actual request is sent immediately. - Preflight: Non-simple methods like PUT/DELETE/PATCH, custom headers like
Authorization, orContent-Type: application/jsoncause the browser to send anOPTIONSrequest first, asking "Is this method/header allowed?" before the actual request.
Remember this: the moment a REST API sends a JSON body, you almost always get a preflight. That accounts for about half of all CORS errors.
Diagnosis Table by Error Message
Find the console message in the table below.
| Console error (verbatim) | Cause type | One-line diagnosis |
|---|---|---|
No 'Access-Control-Allow-Origin' header is present on the requested resource | ① Missing response header | Server isn't sending CORS headers at all |
The value of the 'Access-Control-Allow-Origin' header ... must not be the wildcard '*' when the request's credentials mode is 'include' | ③ Wildcard + credentials conflict | You cannot use * together with cookie auth |
Response to preflight request doesn't pass access control check | ② Preflight failure | OPTIONS response is bad (4xx / missing headers) |
Method PUT is not allowed by Access-Control-Allow-Methods | ④ Allow-Methods missing | PUT is not in the allowed methods list |
Request header field authorization is not allowed by Access-Control-Allow-Headers | ④ Allow-Headers missing | authorization is not in the allowed headers list |
... has been blocked by CORS policy ... contains multiple values '...' | ⑤ Duplicate headers | Proxy and backend both adding the header |
Precise Diagnosis by the 5 Cause Types
① Missing response headers on a simple request
This is the most common one. The server response has no Access-Control-Allow-Origin at all. Open Network tab → the request → Response Headers and check whether access-control-allow-origin is present. If not, CORS config is missing on the server.
② Preflight OPTIONS failure
In the Network tab you'll see a gray OPTIONS request right before the actual request. If that returns 404/405 or has no CORS headers, the actual request never fires. Often the router simply doesn't handle OPTIONS.
③ Credentials + wildcard conflict
The frontend set credentials: 'include' (send cookies) but the server responded with Access-Control-Allow-Origin: *. The browser rejects this because "allow every origin + send cookies" is not allowed for security reasons.
④ Missing Allow-Methods / Allow-Headers
If the preflight response's Access-Control-Allow-Methods or Access-Control-Allow-Headers doesn't cover the actual request, it gets blocked. Classic case: you send an authorization header but the server didn't put it on the allow list.
⑤ Header loss or duplication via proxy/redirect
If a reverse proxy like Nginx and the backend both add CORS headers, you get a multiple values error. Conversely, going through a 301/302 redirect can strip CORS headers. Make exactly one layer responsible for CORS headers.
Demo: the wildcard + credentials trap
This combination does not work.
// ❌ 브라우저가 거부: '*' + 쿠키 동시 사용 불가
fetch('https://api.com/me', { credentials: 'include' })
// 서버 응답: Access-Control-Allow-Origin: *
// Access-Control-Allow-Credentials: true → 에러The correct pattern is to reflect Origin dynamically, but only after whitelist validation.
const allowList = new Set(['https://app.com', 'https://admin.app.com']);
function setCors(req, res) {
const origin = req.headers.origin;
if (allowList.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin); // '*' 아님!
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Vary', 'Origin'); // 캐시 오염 방지
}
}If you omit Vary: Origin, a CDN/proxy can cache one origin's response and serve it to another. Always include it.
Copy-paste configs by server
Express (cors middleware)
const cors = require('cors');
const allowList = ['https://app.com', 'https://admin.app.com'];
app.use(cors({
origin: (origin, cb) => {
// origin이 없는 경우(서버간 호출, 동일 출처)도 허용
if (!origin || allowList.includes(origin)) return cb(null, true);
cb(new Error('Not allowed by CORS'));
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
// cors 미들웨어가 OPTIONS 프리플라이트를 자동 처리합니다.Nginx (add_header + preflight branch)
location /api/ {
set $cors_origin "";
if ($http_origin ~* (https://app\.com|https://admin\.app\.com)) {
set $cors_origin $http_origin;
}
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Max-Age 86400 always;
return 204; # 프리플라이트는 본문 없이 204
}
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
proxy_pass http://backend;
}⚠️ If the backend (Express, etc.) already adds CORS headers, do not add them in Nginx. Putting them in both causes a multiple values error.
Spring Boot (CorsConfigurationSource)
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
// 패턴 사용 시 setAllowedOriginPatterns, 정확한 출처면 setAllowedOrigins
config.setAllowedOriginPatterns(List.of("https://app.com", "https://admin.app.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Content-Type", "Authorization"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}If you use Spring Security, you must call http.cors(Customizer.withDefaults()). Otherwise the security filter blocks the request before CORS config can run.
FastAPI (CORSMiddleware)
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.com", "https://admin.app.com"], # '*' 금지
allow_credentials=True,
allow_methods=["*"],
allow_headers=["Content-Type", "Authorization"],
)If you combine allow_credentials=True with allow_origins=["*"], Starlette blocks it internally—always use an explicit list.
A note from production
The issue I hit most often running a Next.js frontend against a Spring Boot API was type ⑤—duplicate headers. Locally, backend CORS alone worked fine; once we put ALB + Nginx in front in production, the multiple values error exploded. The takeaway: make a single layer responsible for CORS headers. If you use a BFF or API Gateway, handle CORS only at the gateway and turn it off on the backend—debugging gets much easier. For cookie auth, also set SameSite=None; Secure as a pair, or cookies won't survive cross-domain.
Security checklist & debugging order
- ✅ Never use
Access-Control-Allow-Origin: *+credentials: truein production - ✅ Always whitelist-validate Origin, then reflect it
- ✅ Add
Vary: Originto prevent cache pollution - ✅ Add CORS headers in only one of proxy or backend
- ✅ Watch for CORS header loss on endpoints that go through 301/302 redirects
Debugging order: ① Check the Network tab for an OPTIONS request → ② In the failed response's Response Headers, see which Access-Control-* headers are missing → ③ Map to a cause type in the diagnosis table → ④ Apply the copy-paste config for your stack.
FAQ
Q. It works in Postman but I only get a CORS error in the browser. Why? A. CORS is a browser-only policy. Postman/curl do not apply the Same-Origin Policy, so they get a normal response. The server response headers themselves are fine; what's missing are the CORS headers the browser requires.
Q. Can I really not use the * wildcard?
A. For a public API that doesn't send cookies/credentials, * is fine. The moment you use credentials: include (cookie auth), * is forbidden and you must reflect an explicit Origin. A whitelist is also the better security practice.
Q. OPTIONS requests return 404. A. The router isn't handling the OPTIONS method. Express's cors middleware and FastAPI/Spring CORS config handle OPTIONS automatically, but with manual routing you need to add a handler that responds to OPTIONS with 204 + CORS headers.
Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
Comments
Be the first to comment.