CORS Preflight Failing — OPTIONS Returns 404 or 403
Postman works. curl works. The browser fails. That is almost always a missing preflight handler. Browsers send an OPTIONS request before POST, PUT, or DELETE — and your server is not responding to it correctly.
What the browser actually sends first
OPTIONS /api/submit HTTP/1.1 Host: api.example.com Origin: https://app.example.com Access-Control-Request-Method: POST Access-Control-Request-Headers: Content-Type, Authorization
Your server must respond to this OPTIONS request with a 200 or 204 and the correct CORS headers. If it returns 404, the browser stops and shows a CORS error — even though your actual POST endpoint works fine.
What triggers a preflight
Not all requests get a preflight. Simple requests (GET, HEAD, POST with only simple headers like Content-Type: application/x-www-form-urlencoded) skip it. You get a preflight when you use:
- Methods: PUT, DELETE, PATCH
- Headers: Authorization, Content-Type: application/json, or any custom header
- Credentials: any request with credentials: 'include'
Fix by framework
Express
app.options('*', cors()); // Handle all OPTIONS preflights
app.use(cors({ origin: 'https://app.example.com' }));
Nginx
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://app.example.com";
add_header Access-Control-Allow-Methods "POST, GET, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
add_header Access-Control-Max-Age 86400;
add_header Content-Length 0;
return 204;
}
FastAPI
# CORSMiddleware handles OPTIONS automatically — no extra config needed
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_methods=["*"],
allow_headers=["*"],
)
Django
# Install django-cors-headers pip install django-cors-headers # settings.py INSTALLED_APPS = [..., 'corsheaders'] MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware', ...] CORS_ALLOWED_ORIGINS = ['https://app.example.com'] CORS_ALLOW_CREDENTIALS = True
Cache the preflight
Access-Control-Max-Age: 86400 tells the browser to cache the preflight result for 24 hours. Without it, the browser sends OPTIONS before every single request — visible as extra network calls in DevTools.