/개발/Diagnose and Fix the CORS "No Access-Control-Allow-Origin" Error in 5 Minutes
DevelopmentCORS 에러 해결Access-Control-Allow-Origin

Diagnose and Fix the CORS "No Access-Control-Allow-Origin" Error in 5 Minutes

Diagnose the "No Access-Control-Allow-Origin header is present" CORS error in 5 minutes with the Network tab, then fix it with copy-paste code for Express, Spring, and Nginx—including preflight OPTIONS and the credentials include pitfall.

Diagnose and Fix the CORS "No Access-Control-Allow-Origin" Error in 5 Minutes

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.

CODE
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 to
  • from origin 'https://myapp.com' → the origin (frontend) that sent the request
  • No '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, PATCH methods
  • Content-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 statusACAO headerVerdict
No OPTIONS + no ACAO on the actual request responseMissingServer issue — add header config
OPTIONS fails with 404/500-Server issue — OPTIONS route not handled
OPTIONS 200/204 but no ACAOMissingServer issue — preflight response headers missing
ACAO is * while using credentials*Pitfall (see trap ① below)
All headers look fine but still blockedPresentSuspect 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)

JavaScript
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:

JAVA
@CrossOrigin(origins = "https://myapp.com", allowCredentials = "true")
@RestController
public class UserController { ... }

For a clean global setup, the Bean approach is recommended.

JAVA
@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

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 blocked
  • Access-Control-Allow-Headers: allowed custom headers. If Authorization is missing, token requests are blocked
  • Access-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:

JavaScript
fetch('https://api.myapp.com/me', { credentials: 'include' });

the moment the server returns Access-Control-Allow-Origin: *, you get this error.

CODE
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):

JavaScript
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
};

webpack devServer:

JavaScript
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

  1. Read the error messageNo Access-Control-Allow-Origin = confirmed server problem
  2. Network tab — judge frontend vs server from the OPTIONS row / ACAO header / status
  3. Add headers on the server — Express cors(), Spring CorsConfigurationSource, Nginx add_header ... always
  4. Handle preflight — OPTIONS returns 204, with Methods/Headers/Max-Age as a set
  5. If you use credentials — no wildcard; concrete origin + Allow-Credentials true
  6. 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.

확인 정보
✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 · ·업데이트 ·

Comments

Be the first to comment.