Why am I getting a CORS error?
The browser console shows blocked by CORS policy and your fetch() never gets a body. This guide explains what that message 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, HEAD or POST with only safelisted headers, and for POST a
Content-Typeofapplication/x-www-form-urlencoded,multipart/form-dataortext/plain. The browser sends the request, then inspects the response. IfAccess-Control-Allow-Origindoesn't include your origin, the body is dropped. - Preflighted requests: everything else, including a POST with
Content-Type: application/json, any custom header such asAuthorization, and methods like PUT or DELETE. The browser first sends anOPTIONS"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, so 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=["*"],
)
About *: Access-Control-Allow-Origin: * only stops working when the fetch uses credentials: 'include' (cookies, TLS client certificates or browser-managed HTTP auth). Then the server must echo a specific origin and send Access-Control-Allow-Credentials: true. An Authorization header you set yourself in fetch doesn't count as credentials; it works with * as long as the preflight response lists it in Access-Control-Allow-Headers.
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, encoding the target so its own query string survives:
// Before: blocked by CORS
fetch('https://api.example.com/users?page=2')
// After: proxied through corsproxy.dev
const target = 'https://api.example.com/users?page=2';
fetch('https://api.corsproxy.dev/proxy?key=YOUR_KEY&url=' + encodeURIComponent(target))
That's what we built corsproxy.dev to do. The hosted service gives you API keys and daily quotas so the proxy can't be abused on your behalf. We also publish a separate, simpler open-source CORS relay written in Go that you can self-host; it isn't the code behind the hosted service and behaves differently (see self-host vs managed).
Skip the setup
Free tier: 500 requests per day, no credit card. Create a key and you're proxying in under a minute.
It works in Postman but not in the browser
That's expected. Postman, curl and server code don't enforce CORS, so they read any response. The browser does, so the same request can succeed in Postman and fail in your page. A Postman success proves the API works. It doesn't prove CORS is set up.
To reproduce what the browser checks, send the Origin header yourself and look for Access-Control-Allow-Origin in the response:
curl -si https://api.example.com/users -H "Origin: https://app.mysite.com" | grep -i access-control
No output means the browser will block it. For a JSON POST, also test the preflight with -X OPTIONS -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: content-type".
Browser extensions that turn CORS off only change your own browser. Your users will still see the error.
Mixed content looks like a CORS error but isn't
If your page is served over https:// and your code fetches an http:// URL, the browser blocks the request as mixed content. That happens before CORS is evaluated at all, so no CORS header on the server can fix it. The request often never leaves the browser, and fetch() rejects with a generic TypeError, which is why it gets mistaken for a CORS problem.
The console tells them apart. A mixed-content block mentions Mixed Content and says the page "requested an insecure resource"; a CORS failure mentions CORS policy. Read the whole message before changing server headers.
The fix is an https:// endpoint. If the API offers HTTPS, switch the URL. If it doesn't, a proxy can help only because the proxy itself is served over HTTPS and makes the plain-HTTP request from its side, which requires the upstream to be reachable from the proxy's network. A proxy on http:// just moves the same problem.
Other things that look like CORS but aren't
- Network failure. If the server is unreachable, you'll see
Failed to fetch. CORS errors specifically mention CORS. - Cookies not sent.
fetchdoesn't send cookies cross-origin unless you setcredentials: 'include'. To read the response, the server also needsAccess-Control-Allow-Credentials: trueand a specific origin instead of*. See CORS with cookies and credentials.
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.