Guide
CORS explained: cross-origin requests, preflight, and fixes
Your React app on https://app.example.com calls an API at
https://api.example.com. Postman works. curl works. The browser
console shows a red error about CORS and your fetch returns
nothing. That mismatch is one of the most common surprises in modern web
development — and it is working as designed. Cross-Origin Resource
Sharing (CORS) is the browser's contract for when JavaScript on one
origin may read a response from another. This guide explains the same-origin
policy, simple vs preflight requests, the headers that matter, and how to fix
blocked calls without weakening security.
Origins and the same-origin policy
An origin is the combination of scheme, host, and port:
https://solana.garden:443 is one origin;
http://solana.garden is another (different scheme);
https://api.solana.garden is another (different host).
Browsers enforce the same-origin policy for scripts: code loaded from origin A cannot freely read responses from origin B. Without that rule, any website you visit could silently call your bank's API using cookies you already have and exfiltrate the JSON.
CORS is not a firewall and not enforced by curl or server-side code. It is a browser mechanism. The server still receives cross-origin requests; CORS only controls whether client-side JavaScript is allowed to see the response body and headers.
How CORS allows cross-origin reads
When a cross-origin request completes, the browser compares the response headers against the requesting page's origin. If the server explicitly permits that origin, the browser exposes the response to your script. Otherwise the network request may succeed (HTTP 200) but JavaScript gets a generic network error — the classic "CORS blocked" confusion.
The most important response headers:
-
Access-Control-Allow-Origin— which origins may read the response. A specific origin (https://app.example.com) is safest. The wildcard*works only for requests without credentials (cookies or Authorization headers). -
Access-Control-Allow-Methods— permitted HTTP verbs for preflight (GET, POST, PUT, DELETE, etc.). -
Access-Control-Allow-Headers— custom request headers the client may send (for exampleContent-Type,Authorization,X-Request-Id). -
Access-Control-Allow-Credentials— set totruewhen the client usescredentials: 'include'and you need cookies. Requires a specific origin, never*. -
Access-Control-Expose-Headers— which non-simple response headers JavaScript may read (defaults are minimal: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma).
Configure these on the API server (or on a reverse proxy in front of it). Changing frontend code cannot bypass CORS — that would defeat the purpose.
Simple requests vs preflight
Not every cross-origin call triggers a preflight. A simple
request uses GET, HEAD, or POST with a Content-Type of
application/x-www-form-urlencoded,
multipart/form-data, or text/plain, and only
"simple" headers. The browser sends one request; the server answers with
CORS headers on that same response.
A preflight happens when the request is "non-simple" — JSON
bodies (Content-Type: application/json), custom headers like
Authorization, or methods like PUT and PATCH. The browser first
sends an OPTIONS request with
Access-Control-Request-Method and
Access-Control-Request-Headers. The server must respond with
matching Access-Control-Allow-* values and typically HTTP 204.
Only if preflight succeeds does the real request go out.
Preflight adds one round trip. For high-frequency APIs, keep simple GETs
simple where possible, or cache preflight results via
Access-Control-Max-Age (often 600–86400 seconds).
When preflight is skipped entirely
Cross-origin <img>, <script>, and
<link> tags load resources without CORS checks for
display — but you cannot read pixel data from a cross-origin image in a
canvas without crossorigin and proper CORS headers (otherwise
the canvas is "tainted"). WebSockets use a different handshake (see our
WebSockets guide) but still respect origin checks at connection time.
Credentials: cookies and Authorization headers
By default, fetch does not send cookies on cross-origin calls.
To include them, set credentials: 'include' (XHR:
withCredentials = true). The server must reply with
Access-Control-Allow-Credentials: true and echo the exact
requesting origin in Access-Control-Allow-Origin — not
*.
Bearer tokens in an Authorization header are not cookies, but
they still make the request non-simple, so preflight applies. Many OAuth
flows redirect back to your origin with a code; subsequent API calls from
the SPA attach the access token and depend on correct CORS configuration on
the resource server.
A common mistake: enabling credentials on the client while the API returns
Access-Control-Allow-Origin: *. Browsers reject that
combination every time.
CORS is not CSRF protection
CORS stops JavaScript on site A from reading responses from site B. It does not stop site A from sending requests to site B. A classic HTML form POST or an image beacon can still hit your API; the browser just will not let attacker's script inspect the reply.
Protect state-changing endpoints with CSRF tokens, SameSite cookies, or proof-of-possession for APIs — do not treat permissive CORS as authentication. For machine-to-machine traffic, use server-side calls or an API gateway; CORS only applies to browser contexts.
Debugging CORS failures
The console message usually names the blocked origin and the missing header. Work through this checklist:
- Confirm it is CORS. If curl returns 200 with a body but fetch fails, it is almost certainly CORS, not "the API is down."
- Check preflight. In DevTools Network, filter OPTIONS. If preflight returns 404 or lacks Allow headers, fix the server before debugging the POST.
-
Match origins exactly.
https://app.comandhttps://www.app.comare different. Trailing slashes in Allow-Origin are invalid. -
Expose headers you need. If your client reads
X-RateLimit-Remaining, add it toAccess-Control-Expose-Headers. -
Redirects strip CORS. If
http://redirects tohttps://, preflight may fail unless the redirect response also carries CORS headers (prefer HTTPS everywhere).
During local development, a Vite or webpack dev proxy forwards
/api to the backend so the browser sees same-origin traffic —
a legitimate pattern. Disabling web security in Chrome is not; it hides the
problem you will hit in production.
CORS in crypto apps and Solana dApps
Wallet adapters and RPC calls illustrate two different worlds. Wallet extensions inject providers into your page — that is same-origin from the page's perspective. Public Solana RPC URLs, however, are cross-origin from your dApp. Many providers allow browser access with permissive CORS; some block it and expect you to proxy through your backend — partly for API key secrecy, partly for rate control.
If getBalance works in a Node script but fails in the browser,
check whether the RPC endpoint allows your site's origin. A thin server-side
proxy also lets you add caching, backoff on 429 responses, and request
coalescing — patterns that matter when many users share one API key.
Solana Pay and deep links do not involve CORS for the payment itself (the wallet app is a separate application), but your site still needs CORS on any REST endpoint that verifies payments or serves game state to the frontend.
Secure configuration patterns
-
Allowlist origins — maintain an explicit list per
environment (production, staging). Reject unknown origins rather than
reflecting arbitrary
Originheaders unless you understand the risks. -
Separate public and private APIs. Read-only public JSON
might use
*; authenticated routes get strict origins and credentials support. - Terminate CORS at the edge. nginx, Cloudflare, and API gateways can inject consistent headers so application code stays simple.
- Do not use CORS to hide secrets. Any key embedded in frontend JavaScript is public. Proxy sensitive calls server-side.
| Scenario | Preflight? | Typical fix |
|---|---|---|
| GET public JSON API | Often no | Access-Control-Allow-Origin on GET response |
| POST with JSON body | Yes | Handle OPTIONS + Allow-Methods/Headers |
| Cookie session API | Yes | Specific origin + Allow-Credentials: true |
| Third-party RPC from browser | POST preflight | Provider CORS or backend proxy |
Related reading
- OAuth 2.0 and OpenID Connect explained — tokens, redirects, and authenticated API calls
- Solana RPC endpoints explained — browser vs server RPC, 429 fallbacks, and proxies
- WebSockets and server-sent events explained — real-time connections and origin checks
- Connect a Solana wallet to a dApp — wallet flows that sit alongside your API layer