JavaScript CORS Table

PieceWhat it doesField note
Access-Control-Allow-OriginWho may read the responseThe SERVER's grant - * or one exact origin; no lists
Simple vs preflightedGET/POST-basic vs everything elseCustom headers or PUT/DELETE trigger the OPTIONS preflight
OPTIONS preflightThe automatic permission askBrowser asks BEFORE sending; cache via Access-Control-Max-Age
Access-Control-Allow-HeadersPreflight header whitelistAuthorization is NOT simple - adding it preflights every request
Access-Control-Allow-CredentialsCookies cross-origintrue requires an EXACT origin - * with credentials is refused
CORS errors in consoleThe response existedBrowser got it and hid it - CORS blocks READING, not sending
no-cors modeOpaque responsesSends it, gives you an unreadable blob - the escape hatch that barely works
Proxy patternYour server fetchesSame-origin to your backend, which talks to the third party - the production fix
Reference: the MDN CORS reference. CORS is the browser's enforcement of the server's grants: the request usually FLIES, the response arrives, and the browser HIDES it from your JavaScript unless the response carries Access-Control-Allow-Origin naming you. That is why the fix is always server-side - the client cannot grant itself permission. Bottom line: adding Authorization or a custom header silently upgrades you to a preflighted request (the automatic OPTIONS ask), credentials with a wildcard origin are refused by design, and the production answer to uncooperative third parties is your own backend proxy. Related tools: fetch table (the requests CORS gates), security headers table (the header family CORS lives in), and error table (what a CORS failure looks like in console).

CORS is the browser's enforcement of the SERVER's grants: under the same-origin policy, a cross-origin request usually FLIES, the response arrives at the network layer, and the browser HIDES it from your JavaScript unless the response carries Access-Control-Allow-Origin naming your origin.

Bottom line: CORS errors mean the response existed and was confiscated - which is why the fix is always server-side (the client cannot grant itself permission) and why 'it works in Postman' is expected: Postman is not a browser and ignores the policy entirely.

The honest part: adding an Authorization header or a custom header silently upgrades your request to PREFLIGHTED - the browser first sends an automatic OPTIONS ask, and only on approval does the real request go. Teams discover this in production when auth headers turn every request into two.

How to use

  1. Fix CORS on the server: Access-Control-Allow-Origin: https://your.app on the API response - one exact origin beats * the moment credentials are involved.
  2. Tame the preflight: know that custom headers (Authorization, X-*) trigger OPTIONS; cache approvals with Access-Control-Max-Age to stop the double-request tax.
  3. Route uncooperative third parties through your backend: the browser calls your same-origin API, your server calls the third party - CORS never applies server-side.

Frequently asked questions

If CORS blocks my request, did the request even happen?

Yes - this is the most misunderstood part. Under the same-origin policy, a simple cross-origin request is SENT; the server receives it, processes it, and responds. The browser then inspects the response headers and, finding no Access-Control-Allow-Origin for your origin, refuses to hand the response to your JavaScript - surfacing the familiar red console error. Side effects happen (a POST may have written data), which is why preflights exist: for anything with side effects or exotic headers, the browser asks permission with OPTIONS FIRST, before the real request leaves. Never rely on CORS as a security wall for state-changing endpoints - it protects readers, not writers.

Why did adding an Authorization header break my working request?

It upgraded you to preflighted. 'Simple' requests - GET, HEAD, POST with a few basic content types and no custom headers - skip permission checks and just get their responses inspected. The moment you add Authorization, a custom X- header, or use PUT/DELETE, the browser must first ask the server 'may I send this?' via an automatic OPTIONS preflight carrying Access-Control-Request-Headers. If the server does not answer that OPTIONS with matching Allow-Headers (Authorization included), the real request never fires. The fixes: server answers the preflight properly, and Access-Control-Max-Age caches the approval so the double-request tax is paid once.

Why does Access-Control-Allow-Origin: * fail with credentials?

Because the wildcard cannot be trusted with user-scoped data: * tells every origin on the internet it may read the response, and cookies riding along would leak user identity to all of them. The credential contract therefore demands an EXACT origin echo: the server reads the request's Origin header and reflects that one value back, with Access-Control-Allow-Credentials: true. The same rule applies at the cookie level - SameSite=None cookies need Secure. The server-side code is three lines; the security property is that only origins you explicitly list get credentialed access.

What is the production pattern for third-party APIs without CORS headers?

Your own backend becomes the proxy. The browser calls your same-origin endpoint (/api/weather), your server fetches the third-party URL - server-to-server requests have no origin policy, CORS is a BROWSER enforcement - and returns the data. This is also where the API key belongs: shipping a third-party key in frontend code publishes it to every visitor, while the proxy keeps it server-side. Frontend-only workarounds (no-cors mode returning an opaque unreadable response, or public CORS proxy services) range from useless to a supply-chain risk; the backend proxy is the pattern that actually ships.

Related tools