Why Your POST Request Fails but GET Works — CORS & Preflight Explained
A deep dive into CORS, preflight requests, and why browsers block your API calls. Includes a visual flow diagram of how the OPTIONS preflight mechanism works.
What is CORS?
CORS (Cross-Origin Resource Sharing) is a browser security feature. It prevents a webpage on one domain (e.g. your frontend at https://myapp.com) from freely accessing resources on a different domain (e.g. your backend API at https://api.myapp.com).
Without proper setup, the browser blocks such “cross-origin” requests for security reasons. This is the Same-Origin Policy in action — and CORS is the mechanism that lets servers opt in to allowing cross-origin access.
The Classic Symptom: GET Works, POST Fails
You build a frontend and a backend on different origins. Your GET /api/data endpoint works perfectly. Then you add a POST /api/data endpoint with a JSON body — and the browser blocks it with a CORS error. The endpoint itself is fine. So what happened?
Simple Requests vs. Preflighted Requests
The browser classifies cross-origin requests into two categories:
Simple Requests
- Method is
GET,HEAD, orPOST - Only “safe” headers (e.g.
Accept,Content-Typelimited totext/plain,application/x-www-form-urlencoded,multipart/form-data) - No custom headers
The browser sends the actual request directly. The server must still include Access-Control-Allow-Origin in its response.
Preflighted Requests
- Method is
PUT,DELETE,PATCH, etc. - Custom headers like
Authorization,X-Custom-Header Content-Type: application/json
The browser sends an OPTIONS preflight first to ask the server if the real request is allowed.
How Preflight Works — Visual Flow
Here's exactly what happens when your JavaScript makes a cross-origin request:
What's Inside a Preflight Request?
The preflight is an OPTIONS request the browser sends automatically. You never write code for it — the browser does it on its own. It includes:
OPTIONS /api/data HTTP/1.1 Host: api.myapp.com Origin: https://myapp.com Access-Control-Request-Method: POST Access-Control-Request-Headers: Content-Type, Authorization
The server must respond with the appropriate CORS headers:
HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://myapp.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Content-Type, Authorization Access-Control-Max-Age: 86400
Access-Control-Max-Age tells the browser to cache this preflight approval for 86400 seconds (24 hours), so it won't send another OPTIONS for subsequent requests to the same endpoint.
When Does Preflight Happen?
| Condition | Preflight? |
|---|---|
GET with no custom headers | No |
POST with Content-Type: application/x-www-form-urlencoded | No |
POST with Content-Type: application/json | Yes |
Any request with Authorization header | Yes |
PUT, DELETE, PATCH methods | Yes |
Custom headers (X-Custom-Header) | Yes |
Same-origin request (any method) | Never |
How to Fix CORS on Your Backend
Most backend frameworks have CORS middleware. Here are examples for common stacks:
Express.js (Node.js)
const cors = require('cors');
app.use(cors({
origin: 'https://myapp.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));Go (chi router)
import "github.com/go-chi/cors"
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"https://myapp.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 86400,
}))Python (FastAPI)
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://myapp.com"],
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
allow_credentials=True,
max_age=86400,
)Common Mistakes
Access-Control-Allow-Origin: * with credentialsIf your frontend sends cookies or an Authorization header, the server cannot use * as the allowed origin. You must specify the exact origin. Browsers enforce this strictly.
If your server handles POST /api/data but ignores OPTIONS /api/data, preflight fails. CORS middleware usually handles this, but custom route handlers might not.
Access-Control-Allow-HeadersEven if origin and methods are correct, forgetting to allow Content-Type or Authorization in the response headers will cause the preflight to fail.
Quick Debugging Tips
- Open DevTools → Network tab and filter by the failing request. Look for a preceding
OPTIONSrequest. - Check the
OPTIONSresponse headers — areAccess-Control-Allow-Origin,Access-Control-Allow-Methods, andAccess-Control-Allow-Headersall present and correct? - Test with
curlto isolate browser vs. server issues:
curl -X OPTIONS https://api.myapp.com/api/data \ -H "Origin: https://myapp.com" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: Content-Type" \ -v
TL;DR
- CORS = browser security that blocks cross-origin requests unless the server explicitly allows them.
- Preflight = an automatic
OPTIONSrequest the browser sends before “non-simple” requests (JSON body, custom headers, PUT/DELETE/PATCH). - GET works but POST fails? Your server likely isn't responding to the OPTIONS preflight with proper CORS headers.
- Fix: Use CORS middleware on your backend, allow the correct origin, methods, and headers.