How CORS Actually Works and Why Your API Requests Fail

Posted by

Stop guessing and start understanding. Here’s the real reason your cross-origin fetch requests are being blocked (and how to fix them the right way).

Stop guessing and start understanding. Here’s the real reason your cross-origin fetch requests are being blocked (and how to fix them the right way).

Introduction

You run this in your frontend:

fetch("https://api.example.com/data");

…and boom 💥

Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000' 
has been blocked by CORS policy.

You try adding random headers.
You install Chrome extensions.
You even shout at your backend.

But the browser doesn’t care.

CORS short for Cross-Origin Resource Sharing is one of the most misunderstood parts of web development.

This post will explain what CORS really is, how it works step by step, and why your requests fail (even when your code looks fine).


1️. The Core Idea Behind CORS

Let’s start with why CORS exists at all.

By default, browsers follow a Same-Origin Policy (SOP), which means a web page can only make requests to the same domain, port, and protocol it was loaded from.

So:

  • https://myapp.comhttps://myapp.com/api → allowed
  • https://myapp.comhttps://api.example.com → blocked

This protects users from malicious scripts stealing data from other sites.

CORS is the browser’s permission system for breaking that rule safely.

It’s a negotiation between the browser and the server. The server must say,
“Hey, browser, it’s okay, I trust this origin.”


2️. The Two Types of CORS Requests

Not all cross-origin requests are the same.
CORS has two major categories:

1. Simple Requests

These are “safe” requests with:

  • Method: GET, POST, or HEAD
  • Headers: Only basic ones like Accept, Content-Type, etc.
  • Content-Type: application/x-www-form-urlencoded, multipart/form-data, or text/plain

The browser directly sends them, but includes an Origin header.

Example:

Origin: http://localhost:3000

If the response from the server contains:

Access-Control-Allow-Origin: http://localhost:3000

✅ The browser allows it.
❌ If not, it blocks it even if the server responded successfully.


2. Preflighted Requests

If your request includes custom headers or non-standard methods (likePUT, DELETE, or PATCH The browser first sends an OPTIONS request for a “preflight” check.

Example:

OPTIONS /api/data
Origin: http://localhost:3000
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization

Your server must respond:

Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

Only then will the browser send the actual PUT /api/data request.

If the preflight fails → the real request never happens.

💡 You never see this OPTIONS call in your code; it’s handled entirely by the browser.


3️. The Most Common CORS Failure Scenarios

Let’s go through the top ones you’ve probably hit 👇

Case 1: Missing Access-Control-Allow-Origin

Your API responds fine in Postman but fails in the browser.
That’s because Postman ignores CORS browsers that enforce it.

Fix: Add this header from your backend:

Access-Control-Allow-Origin: http://localhost:3000

Or for testing only (⚠️ not in production):

Access-Control-Allow-Origin: *

Case 2: Wrong Allowed Methods

Your API only allows GET, but you send a PUT.

Server must include:

Access-Control-Allow-Methods: GET, POST, PUT

Otherwise, the browser blocks the preflight response.


Case 3: Missing Allowed Headers

If you send custom headers like AuthorizationYou must whitelist them:

Access-Control-Allow-Headers: Content-Type, Authorization

Otherwise, the browser silently drops the request before your server even receives it.


Case 4: Missing Credentials Support

When sending cookies or auth tokens, you must enable credentials explicitly:

Frontend:

fetch("https://api.example.com/data", {
credentials: "include",
});

Backend:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://localhost:3000

⚠️ You cannot use * (wildcard) With credentials, browsers will reject it for security reasons.


❌ Case 5: Misconfigured Proxy Setup

In dev environments (like React or Next.js), you often proxy /api to your backend. If that proxy doesn’t forward the Origin header correctly, CORS fails even if your backend is configured.

✅ Always make sure your dev proxy (like Vite or Webpack) includes changeOrigin: true or is similar.


4️ Visual: How a CORS Request Actually Works

[ Browser (http://localhost:3000) ]
|
| OPTIONS /api/data
| -------------------->
| Origin: http://localhost:3000
|
| <--------------------
| Access-Control-Allow-Origin: http://localhost:3000
| Access-Control-Allow-Methods: GET, POST, PUT
|
| PUT /api/data
| -------------------->
|
| <--------------------
| 200 OK ✅

The preflight (OPTIONS) check ensures safety; the main request runs only if the server grants permission.


5️. Debugging CORS Like a Pro

Step 1: Check the browser console for the full error message.
 It usually tells you exactly which header or method is missing.

Step 2: Inspect the Network tab → OPTIONS request.
 Look for missing or mismatched headers.

Step 3: Confirm your backend is sending correct CORS headers not just for GET, but for all methods.

Step 4: Don’t use Chrome extensions or client-side hacks; they disable CORS for you, not your users.


6️. Real Fix Examples (Backend)

Node.js / Express

import express from "express";
import cors from "cors";

const app = express();
app.use(cors({ origin: "http://localhost:3000", credentials: true }));

NestJS

app.enableCors({
origin: "http://localhost:3000",
credentials: true,
});

Django

CORS_ALLOWED_ORIGINS = ["http://localhost:3000"]
CORS_ALLOW_CREDENTIALS = True

✅ Always fix it on the backend, never in the browser.


7️. Why Postman Works (and Browser Doesn’t)

Because Postman isn’t bound by the Same-Origin Policy, it sends requests directly to the server.

Browsers enforce CORS as a client-side security layer to protect users, not servers.

So when something works in Postman but fails in Chrome, it’s almost always a missing server header.


8️. Quick Recap: The Right CORS Setup


Why CORS Is a Good Thing

CORS errors aren’t bugs; they’re browser safety checks.

Without it, any website could send hidden requests using your credentials to another domain, like a malicious banking site.

CORS exists to protect users, not developers.

Once you understand it, you stop fighting it and start configuring it right.


Conclusion

CORS isn’t a “frontend problem.”
 It’s a contract between your browser and your server.

Remember:

  1. Browser sends Origin
  2. Server responds with matching Access-Control-* headers
  3. The browser decides if it’s allowed

Get that handshake right, and your “CORS policy” errors disappear forever.

Stop guessing. Start configuring.


Call to Action

Still struggling with a CORS issue in your project?
Drop your setup (frontend + backend) in the comments, I’ll help you spot what’s missing 👇

And if your teammate still installs “CORS Unblock” extensions…
Share this post, they’ll finally understand how CORS actually works.

Leave a Reply

Your email address will not be published. Required fields are marked *