CORS Explained: Why Your API Requests Get Blocked (and How to Fix It)
"Blocked by CORS policy" is one of the most common errors in web development — and one of the most commonly misunderstood, since the fix almost never lives where the error appears.
EDTechToolsCenter Editorial"Access to fetch has been blocked by CORS policy" is one of the most common errors in web development, shown up in a browser's console the moment JavaScript on one site tries to call an API on another domain that hasn't explicitly allowed it. It's also one of the most commonly misunderstood errors, because the natural instinct — fix it in the client-side code making the request — is almost never where the actual fix lives. CORS is enforced by the browser, and the header that controls it belongs on the server being called, not the code calling it.
What CORS actually is
CORS stands for Cross-Origin Resource Sharing, and it exists as a controlled exception to a much older browser rule: the same-origin policy, which by default blocks a web page's JavaScript from reading responses from a different origin (a different domain, subdomain, or port) than the one the page itself was loaded from. CORS is the mechanism that lets a server explicitly opt back into allowing specific other origins to read its responses, by sending back specific HTTP headers that tell the browser "this origin is allowed." Without those headers, the browser's same-origin policy applies by default, and the request gets blocked — not by the server refusing to respond, but by the browser refusing to hand the response to the calling JavaScript.
Sponsored
Why the browser blocks cross-origin requests by default
The same-origin policy exists specifically to stop a malicious website from silently making requests to another site — your bank, your email provider, an internal company tool — using your existing logged-in session (since your browser automatically attaches cookies to requests to a domain you're authenticated with) and then reading the response to steal your data. Without this protection, simply visiting a malicious page while logged into another site elsewhere could let that page's JavaScript quietly read your private data from the other site. CORS's default-deny stance is what closes that hole, and a server has to deliberately opt in to being called from another origin, rather than the browser assuming it's fine unless told otherwise.
Simple requests vs preflighted requests
Not every cross-origin request behaves the same way. A "simple" request — a basic GET or POST using only a small set of common headers and content types — goes straight to the server, and the browser only checks the CORS response headers before deciding whether to expose the response to the calling JavaScript. A more complex request — one using a custom header, a method like PUT or DELETE, or a content type outside the simple set — triggers a preflight: the browser automatically sends an OPTIONS request to the server first, asking "would you allow the actual request I'm about to make?" before sending the real request at all. If the server's OPTIONS response doesn't explicitly allow the method, headers and origin involved, the browser never sends the actual request in the first place.
The headers that actually control CORS
- `Access-Control-Allow-Origin` — the core header, specifying which origin(s) are allowed to read the response. A value of `*` allows any origin; a specific origin restricts it to just that one.
- `Access-Control-Allow-Methods` — which HTTP methods (GET, POST, PUT, DELETE, etc.) are permitted for cross-origin requests, checked during a preflight.
- `Access-Control-Allow-Headers` — which custom request headers the client is allowed to send, also checked during a preflight.
- `Access-Control-Allow-Credentials` — whether cookies and credentials can be included in a cross-origin request; this can't be combined with a wildcard origin, since allowing credentialed requests from literally anywhere would defeat the point of the protection.
- `Access-Control-Expose-Headers` — a less commonly known one: by default, client-side JavaScript can only read a small standard set of response headers from a cross-origin response, even once the origin itself is allowed. Any custom header the server wants the client to actually read (a pagination total, a request ID) has to be explicitly listed here, or the JavaScript will find it silently missing despite the response technically containing it.
How to actually fix a CORS error
The fix lives on the server being called, not in the client-side code making the request — no amount of client-side JavaScript configuration can make a server's missing CORS headers appear, since the browser is enforcing a rule about what the server explicitly permitted. The server needs to add the appropriate `Access-Control-Allow-Origin` (and related) headers to its responses, explicitly naming the origin(s) that should be allowed to call it. If you don't control the server being called — a third-party API without CORS support for browser-based calls — the practical options are routing the request through your own backend (which isn't subject to the browser's same-origin policy, since CORS is a browser-enforced rule, not a server-to-server one) or using a proxy specifically during development. Browser extensions that "disable CORS" exist but only affect your own browser locally — they don't fix anything for actual users of a deployed site, and relying on one as a real fix is a common, ineffective workaround.
A concrete walkthrough
Say a page loaded from `https://app.example.com` tries to fetch data from `https://api.otherservice.com`. The browser sends the request (or, if it's a non-simple request, a preflight OPTIONS request first). The server at `api.otherservice.com` processes it and sends back a response — but if that response doesn't include `Access-Control-Allow-Origin: https://app.example.com` (or a wildcard `*`, where credentials aren't involved), the browser discards the response before it ever reaches the calling JavaScript, and the console reports a CORS error. Change nothing except adding that one header on the server's response, and the exact same request succeeds — nothing about the client-side fetch call needs to change at all, since the browser was never objecting to how the request was made, only to what the response did or didn't authorise.
CORS and cookies specifically
Cross-origin requests that need to include cookies (for session-based authentication, rather than a token passed in a header) require an extra explicit step on both sides. The client's request needs to be made with credentials included (`credentials: 'include'` in a fetch call), and the server must respond with `Access-Control-Allow-Credentials: true` alongside a specific, non-wildcard `Access-Control-Allow-Origin` — as mentioned above, a wildcard origin can't be combined with credentials, since that combination would let literally any site read cookie-authenticated responses. Separately, a cookie's own `SameSite` attribute (set by the server issuing the cookie) independently controls whether the browser attaches it to cross-origin requests at all, regardless of CORS headers — `SameSite=None` (combined with `Secure`) is required for a cookie to be sent cross-site in the first place, which is a second, easy-to-miss configuration point distinct from the CORS headers themselves.
Actually debugging a CORS error
The browser's console message is a starting point, not the full picture — the Network tab in browser developer tools shows the actual request and response headers, which is where the real diagnosis happens. Check whether a preflight OPTIONS request was sent and what it returned; check whether the actual response (not just the preflight) carries the expected `Access-Control-Allow-Origin` value matching your exact origin (protocol, domain and port all have to match — `http://` vs `https://`, or a different port, counts as a different origin entirely); and check whether credentials are involved and configured consistently on both the request and the response. Most CORS issues resolve to one of these three checks turning up a missing or mismatched header, rather than anything more exotic.
Framework and platform CORS middleware
Very few teams hand-write raw CORS headers today — most backend frameworks ship a CORS middleware or plugin (Express's `cors` package, Django's `django-cors-headers`, and equivalents across most other stacks) that handles the header logic, including preflight responses, from a small config block rather than manual header-setting on every route. The most common real-world misconfiguration isn't missing CORS support entirely, but a middleware configured too narrowly (allowing only one origin when the app is actually served from several — a production domain and a staging subdomain, for instance) or too broadly (a wildcard origin combined with credentials enabled, which most CORS middleware will actually refuse to do, correctly, until the origin is narrowed to a specific value).
CORS only applies to browsers — not mobile apps or server-to-server calls
This is a genuinely common point of confusion worth stating directly: CORS is a browser-enforced restriction, full stop — it has no equivalent in a native mobile app making an API call, or in one backend server calling another server directly, because there's no browser in either of those pictures enforcing a same-origin policy in the first place. A native iOS or Android app calling an API doesn't hit CORS errors, ever, regardless of how the API's CORS headers are configured, because CORS simply isn't part of how that request is made or evaluated. Similarly, your own backend calling a third-party API server-to-server (not from a user's browser) never encounters a CORS restriction either — which is exactly why routing a browser-based request through your own backend, mentioned earlier, sidesteps the issue: the browser only ever talks to your own origin, and your server's own outbound call to the third party isn't subject to CORS at all. Understanding this scope is what prevents wasted time trying to "fix CORS" in a context where it was never actually the applicable restriction to begin with.
CORS vs authentication — two different jobs
It's worth being clear that CORS is not an authentication or authorization mechanism, and treating a permissive CORS configuration as a security boundary for the server is a mistake. CORS controls whether a browser will let a specific website's JavaScript read a response — it says nothing about whether the request itself was authorized to happen, and a server still needs its own proper authentication and authorization checks regardless of how permissive or restrictive its CORS configuration is. A wide-open `Access-Control-Allow-Origin: *` isn't inherently a security hole by itself for a public, unauthenticated API, but combining a wide-open CORS policy with credentialed requests (cookies, session-based auth) genuinely can be, which is exactly why credentials and a wildcard origin can't be combined in the first place.
Common mistakes
- Trying to "fix" a CORS error by changing client-side JavaScript, when the actual fix requires adding headers on the server being called.
- Setting `Access-Control-Allow-Origin: *` on an API that also relies on cookie-based authentication, which browsers won't allow to be combined with credentials in the first place — and would be a real security risk if it somehow were.
- Relying on a browser extension that disables CORS locally as if it were a real fix, rather than a temporary personal development workaround that does nothing for actual end users.
- Forgetting that a preflight OPTIONS request needs its own correct CORS response — configuring the actual GET/POST endpoint correctly while leaving the OPTIONS handler unconfigured is a common half-fix.
- Assuming a CORS error means the request failed entirely — the server may have processed it completely; the browser is only blocking the JavaScript from reading the result.
The short version: CORS is a browser-enforced rule requiring a server to explicitly opt in, via response headers, to being called by JavaScript from another origin — it exists to stop a malicious site from silently reading your data from another site you're logged into. The fix for a CORS error lives on the server being called, never in the client-side request code, applies only to browser-based requests (not native mobile apps or direct server-to-server calls, both of which never touch a browser's same-origin policy in the first place), and CORS itself is not a substitute for real authentication and authorization — a server still needs its own login and permission checks regardless of how its CORS headers are configured.
Tools used in this article
Sponsored
Frequently asked questions
The server responded, but it didn't include the response headers telling the browser that your site's origin is allowed to read that response — so the browser blocks the calling JavaScript from accessing it.
TechToolsCenter Editorial
How-to Guides
Our editorial desk publishes step-by-step tutorials, comparisons and productivity tips for everyday digital tasks.
Related articles
What Is Rate Limiting, and How Does It Actually Work?
A 429 error isn't your code failing — it's an API telling you, quite specifically, to slow down. Here's what's actually enforcing that, and how to work with it instead of against it.
HTTP Status Codes Explained: The Ones You Actually Need to Know
404 and 500 you already know. But 401 vs 403, 301 vs 302, and 429 trip up developers constantly — and getting them wrong quietly breaks debugging, SEO, and API integrations. Here's what each family actually means and when to use which.
REST vs GraphQL: What's Actually Different, in Plain English
Both fetch data over HTTP. The real difference is who decides what comes back in the response — the server, or the client asking the question.