Diagnose CORS Policy Blocked Errors in 5 Minutes — Fix Code for No Access-Control-Allow-Origin
Seeing a red error in the console and landed here from search? We'll skip the theory lecture and go straight to diagnosis. Three-line summary: CORS is a browser security policy that blocks requests sent to a different origin (protocol + domain + port). The key point: the headers that lift this block must be sent by the server (or Nginx in front of it), not the frontend. In other words, most CORS errors are a backend configuration problem.
Start by Dissecting the Error Message
This is the most common message.
Access to fetch at 'https://api.myapp.com/users' from origin 'https://myapp.com'
has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.Read it phrase by phrase and the diagnosis is done.
Access to fetch at 'https://api.myapp.com/users'→ the target server the request was sent tofrom origin 'https://myapp.com'→ the origin (frontend) that sent the requestNo 'Access-Control-Allow-Origin' header is present→ the root cause: the allow header is missing from the server response
The last line is the conclusion. The server did not send Access-Control-Allow-Origin, so the place to fix is the backend. Note that even when this error appears, server logs often show the request arrived normally and returned 200. The response did arrive, but the browser discarded it instead of handing it to JS because the header was missing.
5-Minute Diagnosis: Pinpoint Responsibility with the Network Tab
Open DevTools → Network tab and click the blocked request. Use this checklist to decide in under 5 minutes.
① Do you see a preflight (OPTIONS) row?
If an OPTIONS method row for the same URL appears first in the request list, a preflight occurred. Preflight is automatically attached under these conditions:
PUT,DELETE,PATCHmethodsContent-Type: application/json- Adding a custom header such as
Authorization
GET/POST (form-encoded) with none of the above is a simple request and goes through without OPTIONS.
② Is Access-Control-Allow-Origin in the Response Headers?
③ What is the status?
| OPTIONS status | ACAO header | Verdict |
|---|---|---|
| No OPTIONS + no ACAO on the actual request response | Missing | Server issue — add header config |
| OPTIONS fails with 404/500 | - | Server issue — OPTIONS route not handled |
| OPTIONS 200/204 but no ACAO | Missing | Server issue — preflight response headers missing |
ACAO is * while using credentials | * | Pitfall (see trap ① below) |
| All headers look fine but still blocked | Present | Suspect frontend credentials / origin typo |
Bottom line: if the header is missing, it is almost certainly a server problem. Changing fetch options on the frontend will not fix it.
Copy-Paste Fix Code by Server
Express (cors middleware)
const cors = require('cors');
app.use(cors({
origin: 'https://myapp.com', // 운영 도메인 명시
credentials: true, // 쿠키/인증 헤더 허용 시
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400, // preflight 캐시 24시간
}));The cors middleware automatically handles OPTIONS preflight with a 204 response. To allow multiple origins, pass an array or a function to origin.
Spring Boot
To open a specific controller only, use the annotation:
@CrossOrigin(origins = "https://myapp.com", allowCredentials = "true")
@RestController
public class UserController { ... }For a clean global setup, the Bean approach is recommended.
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://myapp.com"));
config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
config.setAllowedHeaders(List.of("Content-Type","Authorization"));
config.setAllowCredentials(true);
config.setMaxAge(86400L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}If you use Spring Security, add http.cors(Customizer.withDefaults()) so the Bean above actually applies. Otherwise the filter chain will block OPTIONS first.
Nginx
location /api/ {
add_header Access-Control-Allow-Origin "https://myapp.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Max-Age 86400 always;
if ($request_method = OPTIONS) {
return 204; # preflight는 빈 본문 204로 즉시 응답
}
proxy_pass http://backend;
}Two things matter. Without the always flag, headers are not attached to 4xx/5xx responses, so CORS fires again in error cases. And OPTIONS should be cut off immediately with 204 and no body so it never reaches the backend.
Headers You Must Include on the Preflight Response
OPTIONS preflight is the step that asks "is it OK to send the actual request?" The browser decides whether to proceed based on this response.
Access-Control-Allow-Methods: allowed methods. Without it, PUT/DELETE get blockedAccess-Control-Allow-Headers: allowed custom headers. IfAuthorizationis missing, token requests are blockedAccess-Control-Max-Age: how long to cache the preflight result. Reduces OPTIONS firing on every request
OPTIONS does not receive data, so 204 with no body is the standard.
Two Common Pitfalls
① credentials: 'include' + wildcard (*) is forbidden
This combination blows up often with cookie-based auth. If the frontend sends:
fetch('https://api.myapp.com/me', { credentials: 'include' });the moment the server returns Access-Control-Allow-Origin: *, you get this error.
The value of the 'Access-Control-Allow-Origin' header in the response
must not be the wildcard '*' when the request's credentials mode is 'include'.Fix: specify a concrete origin instead of a wildcard, and pair it with Access-Control-Allow-Credentials: true. Same as the Express/Spring examples above. The cookie itself also needs SameSite=None; Secure to be sent cross-site, so handle those as a set.
A note from production: CORS issues exploded once we split frontend and API with a BFF and microservices. In my case, what ate the most time was not the CORS code but Spring Security filters blocking OPTIONS with 401 first. If OPTIONS is 401 in the Network tab, suspect the auth filter, not CORS config.
② Bypass locally with a dev proxy (not a production fix)
During local development, you can make the browser think it is the same origin via a proxy and bypass CORS entirely.
Vite (vite.config.js):
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
};webpack devServer:
devServer: {
proxy: { '/api': 'http://localhost:8080' },
}The dev server forwards requests starting with /api to the backend, so from the browser's perspective it is same-origin. This does not apply to a production build. In production you must solve it with the server-side CORS config above.
Conclusion: Diagnosis → Fix Cheatsheet
- Read the error message —
No Access-Control-Allow-Origin= confirmed server problem - Network tab — judge frontend vs server from the OPTIONS row / ACAO header / status
- Add headers on the server — Express
cors(), SpringCorsConfigurationSource, Nginxadd_header ... always - Handle preflight — OPTIONS returns 204, with Methods/Headers/Max-Age as a set
- If you use credentials — no wildcard; concrete origin + Allow-Credentials true
- Proxy locally, server config in production — never mix these up
One last action item. Do not spray Access-Control-Allow-Origin: * in production. The error disappears immediately, but you are opening authenticated APIs to any site. Explicitly whitelist allowed origins.
FAQ
Q. The server log shows 200, so why does the frontend still get a CORS error?
A. The response arrived, but the browser blocked it just before handing it to JS because the Access-Control-Allow-Origin header was missing. The server can be working fine and you still get the error if only the header is absent. Add CORS response headers on the server.
Q. The OPTIONS request fails with 401/404. What should I do? A. You are blocked at the preflight stage. A Spring Security (or similar) auth filter is treating OPTIONS as an authenticated request, or the router has no OPTIONS handler. Exempt OPTIONS from auth and return 204.
Q. The local Vite proxy works, but CORS blows up again after deploy. A. The dev proxy only runs on the development server and is not included in the build output. In production you must set CORS headers directly on the backend or Nginx.
Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
Comments
Be the first to comment.