Have you ever built a perfectly good API, only to see a mysterious OPTIONS request appear in your network logs right before the POST or PUT you actually sent?
You think, “Who asked you to do that?”
The browser did. And it’s trying to protect you. This is the story of CORS.
The Fundamental Problem: The Open Web
Let’s say you’re logged into your-bank.com. In another tab, you open a malicious website, evil-site.com. What’s to stop the JavaScript running on evil-site.com from making a request to your-bank.com/api/transfer?
If this were allowed, the malicious site could perform actions on your behalf on any other site where you’re currently logged in, using your browser’s stored cookies and session information.
To prevent this chaos, browsers implement a critical security rule called the Same-Origin Policy.
What is an “Origin”?
The Same-Origin Policy is one of the most important security pillars of the web. An “origin” is defined by the combination of Protocol + Domain + Port.
This means https://example.com is a different origin from:
http://example.com(different protocol)https://api.example.com(different subdomain)https://example.com:5001(different port)
graph TD
subgraph "Origin: https://example.com:443"
A[Protocol: https]
B[Domain: example.com]
C[Port: 443]
end
subgraph "Different Origin"
D[http://example.com]
end
subgraph "Different Origin "
E[https://api.example.com]
end
subgraph "Different Origin "
F[https://example.com:5001]
end
The policy states that a script loaded from one origin cannot read the response from a request sent to a different origin.
Notice the wording: read the response. This is a crucial detail many people miss. The request might still be sent, and the server might even process it, but the browser will block the calling script from accessing the result.
But We Need to Talk to Other Origins!
In modern web development, it’s completely normal for origins to be different. Your frontend might be on my-app.com while your backend API is on api.my-app.com. During development, your frontend on localhost:4200 needs to talk to your backend on localhost:5001.
If the browser blocked this entirely, most web applications would break. This is why CORS (Cross-Origin Resource Sharing) was created.
CORS is simply a mechanism that allows a server to tell the browser, “It’s okay, I trust that other origin to make requests to me.” This is done via HTTP response headers, most famously:
Access-Control-Allow-Origin: https://my-app.com
This header tells the browser, “I will only allow scripts from https://my-app.com to read the response.”
The Mysterious OPTIONS Request
This brings us to the part that confuses everyone. For certain types of requests that the browser considers “non-simple” or potentially destructive, it doesn’t send the real request right away.
These include requests that:
- Use methods like
PUT,DELETE, orPATCH. - Include an
Authorizationheader. - Have a
Content-Typeofapplication/json.
Before sending the actual request, the browser sends a “scouting” request first. This is called a Preflight Request.
It looks like this: OPTIONS /api/orders
This request is the browser asking the server for permission: “Hey, I’m about to send a DELETE request to this URL with an Authorization header and a JSON body. Are you going to allow that?”
The server then responds with a set of CORS headers:
Access-Control-Allow-Origin: https://my-app.comAccess-Control-Allow-Methods: GET, POST, DELETEAccess-Control-Allow-Headers: Content-Type, Authorization
If the server’s response indicates that the upcoming request is allowed, the browser proceeds to send the actual DELETE request. If not, it blocks it, and you see a CORS error in your console.
sequenceDiagram
participant Browser
participant Server
Browser->>Server: OPTIONS /api/orders (Preflight)
note right of Browser: "Can I send a DELETE with Authorization?"
Server-->>Browser: 204 No Content (with CORS headers)
note left of Server: "Yes, DELETE is allowed from your origin."
Browser->>Server: DELETE /api/orders (Actual Request)
Server-->>Browser: 200 OK
This is why you can sometimes see an operation succeed in the database (the DELETE went through), but the frontend still shows a CORS error. The browser blocked your script from reading the success response.
Naima’s Note: As developers building AI-powered frontends, we are constantly interacting with APIs hosted on different origins. The AI model might be served from one cloud provider, the user data from another, and the frontend from a CDN. CORS isn’t just an annoying error to be silenced with
Access-Control-Allow-Origin: *. It’s a fundamental security contract. Understanding it is non-negotiable for building secure, modern applications. At 10xdev.blog, we believe that deep knowledge of the platform—in this case, the browser—is what separates a coder from an engineer.
Simple vs. Preflighted Requests
Not every cross-origin request needs a preflight. Simple Requests (like a basic GET or certain types of POST without custom headers) are sent directly. However, even for these, the browser will still block the response from being read by JavaScript if the server doesn’t reply with the appropriate Access-Control-Allow-Origin header.
A Note on Access-Control-Allow-Origin: *
During development, it’s tempting to just set Access-Control-Allow-Origin: * on your server to “make it work.” This is extremely dangerous in production if your API handles any kind of authenticated or session-based traffic, as it effectively disables the Same-Origin Policy for your API.