The CORS Error That Made Me Actually Understand HTTP

yash kedia
yash kedia
|
Published on 17 May 2026

1. The Error That Started It All

1.1 The Wall Nobody Warned Me About

A few months ago I was building a simple Next.js frontend that talked to a Node.js API running on a different port locally. Straightforward setup — localhost:3000 hitting localhost:4000. The first request I made came back with this:

Access to fetch at 'http://localhost:4000/api/data' from origin 
'http://localhost:3000' has been blocked by CORS policy: No 
'Access-Control-Allow-Origin' header is present on the requested resource.

I had seen this error before. My usual response was to Google it, copy the cors npm package setup from Stack Overflow, paste it in, and move on. This time I did the same thing. It didn't work. That's when I realized I had been treating CORS as a magic incantation for years without actually understanding what it was doing.

1.2 Why the Usual Fix Didn't Work

The Stack Overflow answer I followed told me to add this to my Express server:

const cors = require("cors");
app.use(cors());

Two lines. Usually enough. But my requests were still being blocked. The problem was that I was making a request with a custom Authorization header. That detail changed everything — and I had no idea why, because I had never understood what CORS was actually checking. After an hour of frustration I decided I wasn't going to copy-paste my way out of this one.

1.3 What I Actually Needed to Understand

CORS is not an API feature you turn on. It's a browser security mechanism — and understanding that one sentence reframes everything.
The thing nobody says in the Stack Overflow answers

The browser is the gatekeeper, not the server. Your server just needs to send the right signals back to the browser to let it know which origins are allowed to read its responses. Once I understood that, the error messages started making sense.

2. Understanding What CORS Actually Is

2.1 The Same-Origin Policy

Before CORS, there was the Same-Origin Policy — a rule baked into every browser that prevents JavaScript on one origin from reading responses from a different origin. An origin is the combination of three things: the protocol, the hostname, and the port.

http://localhost:3000   ← origin A
http://localhost:4000   ← origin B (different port = different origin)
https://myapp.com       ← origin C (different protocol and hostname)

The Same-Origin Policy exists for good reason. Without it, a malicious website could make requests to your bank's API using your browser's stored cookies and read the response. CORS is the mechanism that lets servers selectively relax this restriction.

2.2 The Two Types of Requests

Not all cross-origin requests are treated equally. Browsers split them into two categories.

  • Simple requests go through directly. The browser sends the request and checks the response headers. These only qualify if the method is GET, POST, or HEAD and no custom headers are added.

  • Preflighted requests are anything else. The browser first fires an OPTIONS request asking the server if the real request is allowed. Only if the server responds correctly does the actual request go through.

This is exactly what was happening in my case. Adding a custom Authorization header caused the browser to preflight the request — and my server wasn't handling OPTIONS at all.

2.3 What the Headers Actually Mean

The browser sends its origin with every cross-origin request:

Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type

The server responds with what it allows:

Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400

Access-Control-Max-Age tells the browser how long to cache the preflight result — so it doesn't fire an OPTIONS request on every single API call.

3. Fixing It Properly

3.1 Why app.use(cors()) Wasn't Enough

The cors package with no configuration applies a wildcard origin:

Access-Control-Allow-Origin: *

Wildcard works for simple requests. But as soon as you add credentials or custom headers, the rules tighten. A wildcard isn't allowed alongside Access-Control-Allow-Credentials: true. And it doesn't automatically whitelist custom headers like Authorization. My server was sending the wildcard but never handling the OPTIONS preflight at all.

3.2 The Configuration That Actually Works

const cors = require("cors");

const allowedOrigins = [
  "http://localhost:3000",
  "https://myapp.com",
];

app.use(
  cors({
    origin: (origin, callback) => {
      if (!origin || allowedOrigins.includes(origin)) {
        callback(null, true);
      } else {
        callback(new Error("Not allowed by CORS"));
      }
    },
    methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allowedHeaders: ["Content-Type", "Authorization"],
    credentials: true,
    maxAge: 86400,
  })
);

// Handle preflight for all routes
app.options("*", cors());

3.3 The Next.js Side

If your Next.js app exposes an API and needs CORS configured, handle it directly in the route handler:

// app/api/data/route.ts
export async function OPTIONS() {
  return new NextResponse(null, {
    status: 204,
    headers: {
      "Access-Control-Allow-Origin": "https://myapp.com",
      "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
      "Access-Control-Allow-Headers": "Content-Type, Authorization",
      "Access-Control-Max-Age": "86400",
    },
  });
}

export async function GET() {
  return NextResponse.json({ data: "ok" }, {
    headers: { "Access-Control-Allow-Origin": "https://myapp.com" },
  });
}

Or centralize it in middleware.ts if you want it applied across all routes without repeating headers.

4. The Mental Model That Stuck

4.1 Think in Conversations, Not Errors

The shift that made CORS click was thinking of it as a conversation between the browser and server — not a setting to toggle.

Browser → Server:  "I'm http://localhost:3000. Can I POST 
                    with an Authorization header?"

Server  → Browser: "Yes. Here are the methods and headers I allow."

Browser → Server:  [sends the actual request]

When something is blocked, one side of that conversation is broken. Reading the actual response headers in the Network tab — not just the console error — tells you exactly which part failed.

4.2 Three Questions to Diagnose Any CORS Error

  1. Is the request being preflighted? Look for an OPTIONS request in the Network tab before the real one. If it's returning non-2xx, the preflight is failing.

  2. Is Access-Control-Allow-Origin present in the response? If it's missing, the middleware isn't running or isn't matching the route.

  3. Does the header value match the requesting origin exactly? A wildcard won't work with credentials. A trailing slash or wrong protocol won't match.

4.3 Frequently Asked Questions

5. Final Thoughts

Sometimes the best way to fix an error is to stop treating it as something to escape and start treating it as something to understand.

What started as a frustrating afternoon of copy-pasting Stack Overflow answers turned into the most useful HTTP lesson I've had in years. CORS isn't complicated once you see it as what it actually is: a conversation between a browser and a server, governed by a small set of well-defined headers. The cors npm package is still what I reach for — but now when it doesn't work, I know exactly which header to look at and why.

Powered by Synscribe

Loading...
Related Posts
How I Built My Own Loom-Style Screen Recorder

How I Built My Own Loom-Style Screen Recorder

Read Full Story
Solved Video Streaming with ffmpeg and ABS

Solved Video Streaming with ffmpeg and ABS

Read Full Story
© Off the Main Thread 2026