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.
if ($request_method = OPTIONS) blockAccess-Control-Max-Age: 86400app.options('*', cors()) handles global preflight| Scenario | Config |
|---|---|
| nginx OPTIONS handler | location /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.js | app.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.py | CORS_ALLOWED_ORIGINS = ['https://app.example.com']
CORS_ALLOW_HEADERS = ['authorization','content-type','x-request-id']
CORS_ALLOW_CREDENTIALS = True |
| Simple vs preflight triggers | Simple: 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 |
Non-simple request. Simple: GET/HEAD/POST + only standard headers. Any custom header, PUT/DELETE/PATCH, Content-Type not in whitelist → preflight.
Access-Control-Max-Age: 86400 caches preflight for 24h. Browser won't repeat OPTIONS per request.
Yes. JWT in Authorization header → custom header → preflight required. DRF-cors settings must include "authorization".
<a href="/en/cors">Enterno CORS checker</a> → URL + Origin → see preflight response + headers.