Why am I getting a CORS error?
The browser console shows blocked by CORS policy and your fetch() never gets a body. What that actually means, why it happens, and three ways to fix it.
The error you're staring at
It looks something like this:
Access to fetch at 'https://api.example.com/users' from origin
'https://app.mysite.com' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.
The frustrating bit: your server did respond. If you check the Network tab, you'll often see a 200 with the body you wanted. The browser just refuses to hand the response to your JavaScript.
What CORS actually is (briefly)
CORS — Cross-Origin Resource Sharing — is a security rule the browser enforces, not the server. When JavaScript running on app.mysite.com tries to fetch from api.example.com, the browser checks whether the response is allowed to be shared across origins. The check happens in two ways:
- Simple requests (GET/POST with no custom headers) — the browser sends the request, then inspects the response. If
Access-Control-Allow-Origindoesn't include your origin, the body is dropped. - Preflighted requests (anything with a non-trivial header or method) — the browser first sends an
OPTIONS"preflight" asking "can my origin do this?". If the server doesn't reply with the rightAccess-Control-Allow-*headers, the actual request never goes out.
The server-side origin doesn't care about CORS. curl, server-to-server requests, and mobile apps don't trigger it. It is purely a browser-side safety net to stop one site from quietly reading another site on the user's behalf.
Why your server is "refusing"
Almost always one of three things:
- The server returns the response, but no
Access-Control-Allow-Originheader — the browser blocks the JS from reading it. - The server returns
Access-Control-Allow-Origin: https://other.exampleand your origin doesn't match. - The request was preflighted; the server's
OPTIONSresponse is missing or doesn't include the method/header your fetch is using.
It's worth opening DevTools → Network and looking at both the failed request and the OPTIONS right before it. The headers tell you exactly which check failed.
Three ways to fix it
Option 1 — Configure CORS on the server (the real fix)
If you control the API, this is the right answer. Examples:
Node / Express:
import cors from 'cors';
app.use(cors({
origin: ['https://app.mysite.com', 'http://localhost:3000'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
Go (net/http):
func cors(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "https://app.mysite.com")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
Python / FastAPI:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.mysite.com"],
allow_methods=["*"],
allow_headers=["*"],
)
Use specific origins, not *, the moment you start sending credentials or auth headers — browsers refuse the combination.
Option 2 — Proxy through your own backend
If the API is third-party and doesn't return CORS headers (lots of older APIs and partner systems), call it from your own server instead. Your frontend hits app.mysite.com/api/forward, your backend forwards the request to the third party, and you set CORS on your own endpoint however you like.
This is the cleanest answer for production, because you also get a place to put rate limiting, secrets, and observability.
Option 3 — Use a CORS proxy
For development, prototypes, and "we just need to ship this", a CORS proxy in the middle does the same thing without you having to host anything. You replace the URL:
// Before — blocked by CORS
fetch('https://api.example.com/users')
// After — proxied through corsproxy.dev
fetch('https://api.corsproxy.dev/proxy?url=https://api.example.com/users', {
headers: { 'X-API-Key': 'sk_live_...' }
})
That's what we built corsproxy.dev to do. The hosted version gives you API keys and rate limits so the proxy can't be abused on your behalf; the open-source version is the same Go binary you can run yourself.
Skip the setup
Free tier: 100 requests per day, no credit card. Create a key and you're proxying in under a minute.
Things that look like CORS but aren't
- Mixed content.
https://page fetchinghttp://resource — browsers block this with a separate error. The fix is HTTPS, not CORS. - Network failure. If the server is unreachable, you'll see
Failed to fetch. CORS errors specifically mention CORS. - Cookies not sent. If you need cookies cross-origin, you also need
credentials: 'include'on the fetch andAccess-Control-Allow-Credentials: trueon the response — and the server can no longer use*for origins.
One sentence summary
CORS errors come from the browser refusing to share an API response across origins because the server didn't say it was allowed. The fix is to either change the server, route the request through your backend, or use a proxy that does the same.