CORS Debugger

CORS Debugger

Debug CORS issues, analyze headers, generate server configurations. Support for Express, Nginx, Apache, Django, Flask, Spring Boot. Learn CORS concepts. Free online CORS tester

CORS errors are almost never about CORS — they are about misreading which side of the request fails. The browser shows "blocked by CORS policy" in red, you change the server, the error stays, because the actual problem was a missing preflight handler or a credentialed request hitting Access-Control-Allow-Origin: *. This debugger walks the request through the same algorithm the browser uses (Fetch spec §3.2), tells you the exact step that fails, and generates correct server config for Express, Nginx, Apache, Django, Flask, and Spring.

What CORS actually decides

CORS does not block requests. The browser sends the request, gets a response, then refuses to expose the response to JavaScript if the headers do not authorize it. The network tab shows status 200; the JS callback gets an opaque error. This is why "the request reaches the server but my code says CORS failed" is the most common symptom.

Two flavors: simple requests (GET/HEAD/POST with safe Content-Type) go directly; everything else triggers a preflight OPTIONS request first. The preflight is a separate request the browser makes to ask permission; if the server does not handle OPTIONS, the real request is never sent.

Working example: a credentialed POST that "fails CORS"

Input

fetch("https://api.example.com/users", {
  method: "POST",
  credentials: "include",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Ada" })
})

Output

Browser sends preflight: OPTIONS /users with
  Origin: https://app.example.com
  Access-Control-Request-Method: POST
  Access-Control-Request-Headers: content-type

Server must respond with:
  Access-Control-Allow-Origin: https://app.example.com   (NOT *, because credentials=include)
  Access-Control-Allow-Methods: POST
  Access-Control-Allow-Headers: content-type
  Access-Control-Allow-Credentials: true
  Vary: Origin

Then the real POST is sent and its response must also include:
  Access-Control-Allow-Origin: https://app.example.com
  Access-Control-Allow-Credentials: true

With credentials: "include", Access-Control-Allow-Origin: * is invalid — the browser rejects the response. You must echo the specific origin and add Vary: Origin so caches do not poison the response for other origins.

The rules nobody remembers correctly

  • A wildcard origin (*) cannot be used with credentials. If credentials: include is set on the client and Allow-Origin is *, the browser drops the response. Either reflect the origin or refuse credentials.
  • Preflight is cached per (origin, method, headers) tuple. Access-Control-Max-Age sets the cache duration in seconds. Chromium caps at 7200 (2h); Firefox at 86400 (24h); Safari at 600. Production usually wants the max; misconfiguration is hidden when the preflight is cached.
  • Custom headers (X-Auth-Token) trigger a preflight. Even GET with a custom header is not a "simple" request.
  • Content-Type: application/json triggers a preflight because it is not on the safelist (text/plain, application/x-www-form-urlencoded, multipart/form-data are).
  • The Origin header is sent on every cross-origin request, NOT just preflight. Reading Origin server-side is the right way to log "who is hitting me from where".
  • Same-origin = same scheme + same host + same port. https://api.example.com and http://api.example.com are different origins. So are api.example.com:8443 and api.example.com:443.

When to reach for this tool

  • You added a new endpoint and the browser reports "CORS error" — paste the failing fetch + your server config and see which step actually fails.
  • You inherited a microservice with hand-written CORS headers that "mostly work" and want a reference config that handles preflight, credentials, and Vary correctly.
  • You are migrating from a same-origin setup (frontend served from the API) to a separate domain and need to verify the CORS rules before flipping DNS.
  • You are debugging "the cookie is not being sent" — usually credentials: include is missing on the client OR Allow-Credentials is missing on the server, OR SameSite=Lax cookie policy is blocking it.

What this tool will not do

  • It will not make a real request to your server. CORS is enforced by the browser; testing against the network without a browser context misses the policy enforcement. Verify the generated config in your actual app.
  • It will not catch private-network CORS issues. Newer browsers also gate requests from public sites to private IPs (PNA / Local Network Access). Different headers, different policy, separate debugging.
  • It will not fix CSRF. CORS protects responses; CSRF protects against forged requests with cookies. Different attack surface — disable one and the other is still relevant.

All analysis runs in your browser. URLs and headers you paste stay local.

Frequently asked questions

Why does no-cors work for an image but not for fetch JSON?

no-cors mode produces an "opaque" response — your JS cannot read the body, but the request is sent (and the browser may use it for purposes like setting an image src). Useful for fire-and-forget images and tracking pixels. Useless for fetching data — you cannot read it.

Can I bypass CORS in development?

Yes — use a dev proxy. Vite/webpack-dev-server's proxy option forwards /api/* to your backend over the same origin from the browser's perspective. No CORS headers needed. Production needs real CORS config; do not ship the proxy.

My OPTIONS request returns 401. Is that the bug?

Yes. Preflight requests should never require authentication — they ask the browser for permission to send the real request, before any auth state exists. Configure your server to allow OPTIONS without auth (most frameworks expose this as a config flag).

Should I use a wildcard Allow-Origin?

For public, non-credentialed APIs (CDN, public data): * is fine. For anything with cookies, tokens in headers, or user-specific data: echo a specific origin and validate against an allowlist. Wildcards plus credentials is a bug per the spec.

What does Vary: Origin do?

Tells caches (browser, CDN, proxies) that the response varies by the Origin header. Without it, a CDN may cache the response for one origin and serve it to another, breaking CORS for the second. Required whenever Allow-Origin reflects the request.

Why does Authorization header need to be in Allow-Headers?

Custom request headers (anything outside CORS-safelisted: Accept, Accept-Language, Content-Language, Content-Type with safe value) must be explicitly allowed. Authorization is not safelisted. List it in Access-Control-Allow-Headers (preflight response) and the browser permits the actual request to send it.

Related tools

Last updated · E-Utils editorial team