Tutorial

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.

· ~8 min read · Updated Nov 12, 2026

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:

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:

  1. The server returns the response, but no Access-Control-Allow-Origin header, so the browser blocks the JS from reading it.
  2. The server returns Access-Control-Allow-Origin: https://other.example and your origin doesn't match.
  3. The request was preflighted; the server's OPTIONS response 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.

Get a free key →

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

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.