CORS is a browser mechanism that lets a server declare which other origins may call it, relaxing the same-origin policy on purpose. Two reframes dissolve most of the confusion: the browser enforces it (not the server, which is why curl works), and the server fixes it (not your frontend, which is why no amount of JavaScript helps). Everything else is headers.
Key takeaways
| Question | Answer |
|---|---|
| The foundation | Same-origin policy: scripts can’t read other origins’ responses by default |
| An origin | scheme + host + port — all three must match |
| CORS | Server headers opting specific origins in; browser enforces |
| Preflight | OPTIONS “may I?” before non-simple requests |
| The eternal truth | CORS errors are fixed on the server, full stop |
The whole protocol, on the wire
# Preflight: the browser asks before a PUT with an auth header
OPTIONS /classes/Product HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: x-parse-session-token
# The server's permission slip
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com ← this origin may read
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: x-parse-session-token
Access-Control-Max-Age: 86400 ← cache this answer; skip tomorrow's preflight
# Then, and only then, the real request flies.
What it looks like from application code — and the platform truth the tabs teach: CORS is a browser concern, which native apps never meet:
// Browser JavaScript — Back4app JS SDK
// A cross-origin call that just works: the platform answers the
// preflight and sends the CORS headers, so the browser lets it through
Parse.initialize('APP_ID', 'JS_KEY');
Parse.serverURL = 'https://parseapi.back4app.com'; // different origin than your site
const products = await new Parse.Query('Product').find();
// No proxy hacks, no "disable CORS" — the server side is configured correctly. // Flutter / Dart — Back4app Flutter SDK
// Native apps have no same-origin policy — CORS is a browser concern.
// Flutter Web, however, DOES enforce it: same browser rules apply there.
final query = QueryBuilder<ParseObject>(ParseObject('Product'));
final response = await query.query();
// Works identically on mobile and web because the server sends CORS headers. // iOS / Swift — Back4app Swift SDK
// No browser, no same-origin policy: CORS never applies to native iOS.
// The same backend serves browsers (with CORS headers) and apps alike.
let query = Product.query()
query.find { result in
if case .success(let products) = result { render(products) }
} // Android / Kotlin — Back4app Android SDK
// No browser, no same-origin policy: CORS never applies to native Android.
// The same backend serves browsers (with CORS headers) and apps alike.
val query = ParseQuery.getQuery<ParseObject>("Product")
query.findInBackground { products, e ->
if (e == null) render(products)
} The flow, decided
Two details carry most debugging sessions: a simple request (GET/HEAD/POST with safelisted headers and plain content types) skips the preflight — which is why adding an Authorization header suddenly “breaks” an endpoint that worked; and credentialed requests tighten everything — cookies flow only with Access-Control-Allow-Credentials: true plus an exact origin, never the wildcard, per the spec.
How to fix common CORS errors (error → cause → fix)
| Console says | Actually happened | Fix (always server-side) |
|---|---|---|
| No ‘Access-Control-Allow-Origin’ header | Server never opted your origin in | Add your origin to the allowed list |
| Origin not allowed by Access-Control-Allow-Origin | Allow-list exists; you’re not on it | Add the exact scheme+host+port |
| Response to preflight… doesn’t pass | OPTIONS unhandled or headers incomplete | Answer OPTIONS with methods/headers |
| Wildcard ’*’ cannot be used with credentials | Cookies + * — forbidden combination | List origins explicitly |
| Request header not allowed | Custom header missing from allow-list | Add it to Access-Control-Allow-Headers |
And the anti-fix worth naming: browser-flag disabling and permissive dev proxies make the error invisible on your machine only — the deploy still breaks for users. The real fix is a server configuration measured in minutes.
Common use cases
- SPA + API on different origins — app.example.com calling api.example.com: the everyday case CORS exists for.
- Local development — localhost:3000 against a real backend; allow the dev origin, don’t disable the shield.
- Public APIs — wildcard origin, no credentials: correct and safe for genuinely public data.
- Multi-frontend platforms — several apps, one backend, an explicit origin list per environment.
- BaaS backends — the platform answers preflights and sends the headers; your job reduces to declaring allowed origins.
Wildcard vs. explicit origins: a decision matrix
| Configuration | Right when… | Never when… |
|---|---|---|
Wildcard * | Public, credential-free API | Cookies or user sessions exist |
| Explicit origin list | Private APIs, credentialed apps | — (this is the default answer) |
| Reflecting request origins | Almost never | Combined with credentials — the classic hole |
| Per-environment lists | Dev/staging/prod hygiene | Prod inheriting dev’s localhost entries |
One security reframe closes the matrix: CORS is not access control — native apps and servers ignore it entirely, so it protects browser users from malicious sites, not your API from callers. Authentication and data-layer permissions do that job; CORS just decides which websites’ scripts may read the answers.
Limitations and trade-offs
- It only governs browsers. Anything non-browser walks past it — never mistake an origin list for authorization.
- Preflights cost a round trip on non-simple requests;
Access-Control-Max-Agecaching is the cheap, forgotten fix. - Misconfiguration fails closed and loud — good for safety, brutal for debugging without the error→fix table above.
- Origin lists are environment state. Staging origins leak into production configs; audit the list like any credential.
- CORS ≠ CSRF ≠ CSP. Neighboring acronyms, separate defenses — cookies still need SameSite/token protection, and pages still need a content policy, with CORS handling only the cross-origin read.
CORS on Back4app
Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. The CORS chore ships handled: platform APIs answer preflights and send correct headers, so browser apps call the backend from allowed origins without proxy hacks — the JavaScript tab above is the entire experience — while the Flutter, Swift, and Kotlin tabs demonstrate the quieter truth that native clients never meet the ceremony at all. Real security stays where it belongs: sessions, ACLs, and class-level permissions enforced server-side on every request, from every origin.
Frequently asked questions
What is CORS in simple terms?
The browser's permission system for cross-site API calls. By default, scripts on one origin cannot read responses from another — the same-origin policy. CORS is how a server opts in: response headers declaring which origins, methods, and headers it accepts. The browser enforces; the server declares; your frontend code is just the messenger.
What is an origin, exactly?
The triplet of scheme, host, and port. https://app.example.com and https://api.example.com are different origins (host differs); so are http and https versions of one site (scheme differs), and :3000 vs :8080 in development (port differs). Every CORS decision compares these three parts — nothing else about the URL matters.
What causes the classic CORS error?
The browser called a different origin, and the response lacked an Access-Control-Allow-Origin header matching yours — so the browser blocked your script from reading it. The request often reached the server fine; the block is client-side, on the read. That is why the fix is always server configuration, never frontend code.
What is a preflight request?
The browser's advance permission check for non-simple requests: before sending a PUT, a DELETE, or anything with custom headers like an authorization token, it sends an OPTIONS request asking "may I?" The server's headers answer which methods, headers, and origins are allowed; only then does the real request fly. Preflights are cacheable via Access-Control-Max-Age.
Why does my request work in curl but fail in the browser?
Because CORS is browser enforcement, not server rejection. Tools like curl and native mobile apps have no same-origin policy, so they read the response happily. The browser applies the policy on behalf of its user — the difference you are seeing is the enforcement point, and it is also the proof that CORS is not access control.
Is Access-Control-Allow-Origin: * safe?
For genuinely public, credential-free APIs, yes. The wildcard is forbidden in credentialed mode — browsers refuse cookies against it by design — and reflecting arbitrary origins while allowing credentials is the classic misconfiguration that turns CORS from a shield into a hole. Private APIs list origins explicitly.
Can I just disable CORS to fix the error?
Only in the sense that removing a smoke alarm fixes a fire. Browser flags and permissive proxies mask the symptom on your machine while shipping the breakage to every user. The correct fix takes minutes: configure the server (or platform dashboard) to allow the origins that should call it.
What is the difference between CORS, CSRF, and CSP?
Three different jobs. CORS governs which origins may read responses from a server. CSRF is an attack — riding a user's cookies to forge requests — countered by tokens and SameSite cookies, not by CORS. CSP is a page's own policy restricting what it may load and execute, the anti-XSS tool. Adjacent acronyms, orthogonal mechanisms.