← Back to Blog
Backend FundamentalsMarch 31, 20268 min read

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?

The browser never even sent your POST request. It sent a preflight OPTIONS request first, and the server didn't respond with the right headers — so the browser blocked the real request before it left.

Simple Requests vs. Preflighted Requests

The browser classifies cross-origin requests into two categories:

Simple Requests

  • Method is GET, HEAD, or POST
  • Only “safe” headers (e.g. Accept, Content-Type limited to text/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:

CORS Request FlowJavaScript calls fetch() / AxiosIs this a cross-origin request?NoSend directlyYesIs it a "simple" request?YesSend actual request(server must include ACAO)NoBrowser sends OPTIONS preflightAccess-Control-Request-Method: POSTServer responds to OPTIONS with:Access-Control-Allow-Origin: https://myapp.comAccess-Control-Allow-Methods: POST, PUT, DELETEHeaders match? Origin allowed?NoCORS Error!Browser blocks the requestYesBrowser sends actual POST requestServer sends response with dataResponse available to JSAllowedPreflightBlockedRequest/Response

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:

Preflight Request (sent by browser)
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:

Preflight Response (from server)
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?

ConditionPreflight?
GET with no custom headersNo
POST with Content-Type: application/x-www-form-urlencodedNo
POST with Content-Type: application/jsonYes
Any request with Authorization headerYes
PUT, DELETE, PATCH methodsYes
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

1. Using Access-Control-Allow-Origin: * with credentials

If 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.

2. Handling only the main route, not OPTIONS

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.

3. Missing Access-Control-Allow-Headers

Even 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

  1. Open DevTools → Network tab and filter by the failing request. Look for a preceding OPTIONS request.
  2. Check the OPTIONS response headers — are Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers all present and correct?
  3. Test with curl to 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 OPTIONS request 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.