Skip to content

How to Fix CORS Preflight

Key idea:

CORS preflight — an OPTIONS request browser sends before a non-simple request (custom headers, non-GET). If the server does not answer 200/204 with proper CORS headers, the actual request never runs. Common fixes: respond to OPTIONS in nginx, add Access-Control-Allow-Headers with your custom headers, set Access-Control-Max-Age.

Below: step-by-step, working examples, common pitfalls, FAQ.

Step-by-Step Setup

  1. Confirm it's a preflight: DevTools → Network → OPTIONS before the actual request
  2. Inspect OPTIONS response: status 200/204 + proper CORS headers
  3. nginx: dedicated if ($request_method = OPTIONS) block
  4. Ensure Access-Control-Allow-Headers lists all your custom ones (X-Request-Id, Authorization)
  5. Max-Age caches preflight: Access-Control-Max-Age: 86400
  6. Express: app.options('*', cors()) handles global preflight
  7. Verify: Enterno CORS checker — shows preflight response

Working Examples

ScenarioConfig
nginx OPTIONS handlerlocation /api/ { if ($request_method = OPTIONS) { add_header 'Access-Control-Allow-Origin' '$http_origin'; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE'; add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Request-Id'; add_header 'Access-Control-Max-Age' 86400; return 204; } # ... actual proxy_pass }
Express.jsapp.use(cors({ origin: 'https://app.example.com', methods: ['GET','POST','PUT','DELETE'], allowedHeaders: ['Authorization','Content-Type','X-Request-Id'], credentials: true, maxAge: 86400 }));
Django settings.pyCORS_ALLOWED_ORIGINS = ['https://app.example.com'] CORS_ALLOW_HEADERS = ['authorization','content-type','x-request-id'] CORS_ALLOW_CREDENTIALS = True
Simple vs preflight triggersSimple: GET + only Accept+Content-Type (whitelist) Preflight: PUT, PATCH, DELETE, or any custom header
Preflight caching (reduce load)Access-Control-Max-Age: 86400 # browser caches preflight 24h

Common Pitfalls

  • OPTIONS returns 405 (Method Not Allowed) — the server isn't handling OPTIONS specially
  • Access-Control-Allow-Headers missing your custom header — preflight fails
  • Wildcard * with credentials=true — browser blocks
  • Missing Access-Control-Max-Age → browser re-sends OPTIONS for every request = 2× latency
  • CORS via PHP header() after session_start() can be overridden by proxies

Learn more

Frequently Asked Questions

When does the browser do preflight?

Non-simple request. Simple: GET/HEAD/POST + only standard headers. Any custom header, PUT/DELETE/PATCH, Content-Type not in whitelist → preflight.

Preflight is slower — how to speed up?

Access-Control-Max-Age: 86400 caches preflight for 24h. Browser won't repeat OPTIONS per request.

Django DRF + JWT — does preflight need Authorization?

Yes. JWT in Authorization header → custom header → preflight required. DRF-cors settings must include "authorization".

How to test?

<a href="/en/cors">Enterno CORS checker</a> → URL + Origin → see preflight response + headers.