What Happens When You Type a URL and Press Enter
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →That red "blocked by CORS policy" message is the browser doing its job. Here is what it protects, why preflight requests fail, and how to fix CORS on the server for good.
Sooner or later every web developer meets this line in the console:
Access to fetch at 'https://api.example.com/users' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
The usual reaction is to search for a quick fix, paste in mode: 'no-cors' or install a browser extension, and move on. Then it breaks again in production. Let's fix CORS errors properly, and that starts with knowing what CORS is actually doing.
Browsers enforce the same-origin policy: a page loaded from one origin can't freely read responses from another. An origin is scheme + host + port. So http://localhost:3000 and http://localhost:4000 are different origins, and so are https://example.com and https://api.example.com.
CORS (Cross-Origin Resource Sharing) is how a server opts in and says "that origin is allowed to read my responses." It's nothing more than a handful of HTTP response headers. Which leads to the one sentence worth remembering:
CORS is fixed on the server, not in your frontend code.
Your request usually does reach the server. The browser simply refuses to hand the response to your JavaScript because the server never granted permission. That's also why the same call works fine in Postman or curl. They aren't browsers, so they don't enforce the policy.
Some requests go straight through. A GET, or a POST with basic headers and a form-style content type, counts as a "simple" request.
Almost everything a modern app sends triggers a preflight instead. Before the real request, the browser fires an OPTIONS request asking for permission. Common triggers:
Content-Type: application/jsonAuthorization headerPUT, PATCH or DELETEX-Request-IdIf the server doesn't answer that OPTIONS request with a 2xx status and the right headers, the real request never happens. A huge share of "my POST fails with CORS" bugs are really "my server returns 404 or 401 for OPTIONS."
| Header | What it does |
|---|---|
Access-Control-Allow-Origin |
Which origin may read the response: one exact origin, or * |
Access-Control-Allow-Methods |
Methods allowed for the real request (sent on the preflight) |
Access-Control-Allow-Headers |
Request headers allowed, e.g. Content-Type, Authorization |
Access-Control-Allow-Credentials |
true when cookies or credentials are included |
Access-Control-Max-Age |
How long the browser may cache the preflight answer, in seconds |
Express (Node.js) with the cors package:
import express from 'express'
import cors from 'cors'
const app = express()
const allowed = ['http://localhost:3000', 'https://www.yoursite.com']
app.use(cors({
origin: (origin, cb) => {
// no Origin header = server-to-server call or a tool like curl
if (!origin || allowed.includes(origin)) return cb(null, true)
cb(new Error('Not allowed by CORS'))
},
credentials: true,
}))
Register it before your routes and before auth middleware. Otherwise your auth layer rejects the preflight (which never carries your token) and you get a CORS error that's really an auth error.
Next.js route handlers: set the headers on the response and export an OPTIONS handler that returns them as well. Even better, if the frontend and API live in the same Next.js app, call relative URLs like /api/users. No cross-origin request, no CORS question.
Nginx in front of your app: add the headers in the location block and use always, so they're sent on error responses too:
add_header Access-Control-Allow-Origin "https://www.yoursite.com" always;
add_header Vary Origin always;
If you send cookies (fetch(url, { credentials: 'include' }), or withCredentials: true in Axios), two rules kick in:
Access-Control-Allow-Origin cannot be *. It must echo the exact requesting origin.Access-Control-Allow-Credentials: true.When you echo the origin dynamically, also send Vary: Origin. Without it, a CDN can cache the response for one origin and serve it to another.
mode: 'no-cors' doesn't bypass anything. It returns an opaque response your code can't read, so things fail silently instead of loudly.Access-Control-Allow-Origin: * everywhere is fine for a genuinely public, read-only API. For anything touching user data, allow-list specific origins.In local development you can skip cross-origin calls entirely with a proxy. Vite's server.proxy or Next.js rewrites forward /api/* from your dev server to your backend. The browser only ever sees one origin. In production, either serve both under one domain or configure real CORS headers.
OPTIONS request just before it?Origin header with the response's Access-Control-Allow-Origin. They must match exactly: protocol, host, port, no trailing slash.Allow-Credentials: true is present.Postman isn't a browser and doesn't enforce the same-origin policy. The server responds the same way to both; only the browser checks for CORS headers before letting your code read the response.
Not really. You can avoid cross-origin requests with a dev proxy or by serving everything from one domain, but the permission itself has to come from the server's response headers.
No. CORS protects users' browsers from malicious sites reading data on their behalf. It does nothing to stop curl, scripts or other servers. You still need authentication, input validation and rate limiting.
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →There are dozens of HTTP status codes, but about fifteen cover nearly everything. What each one means, when to return it, and the mix-ups (401 vs 403, 301 vs 302) that confuse everyone.
Read →A, AAAA, CNAME, MX, TXT and NS records in plain English, why DNS changes seem slow, and the troubleshooting commands that tell you what is really going on.
Read →