Tags: web-dev concept

CORS

Date: 2026-08-16


The mechanism by which a server permits other origins to read its responses. Every “blocked by CORS policy” error is the server declining, not your request being malformed — which is why fixing it in the front end never works.


What it is

CORS — Cross-Origin Resource Sharing — is a set of HTTP headers a server sends to tell the browser which origins may read its responses, relaxing the same-origin policy in a controlled way.

The enforcement is entirely in the browser. A server without CORS headers is not protecting itself — curl, Postman and any backend can read it freely. CORS protects users from having their authenticated data read by sites they didn’t authorise.

The simple case

BROWSER                                    SERVER (api.example.com)

fetch('https://api.example.com/data')  ─▶
  Origin: https://shop.example.com

                                       ◀─  200 OK
                                           Access-Control-Allow-Origin:
                                             https://shop.example.com

browser checks the header against the page's origin
  match   → JavaScript receives the response
  no match → JavaScript receives an error. The response was fetched
             and then discarded.

The request was sent and the server did the work. The browser threw the response away before your code saw it. This is why a failing CORS request can still create a record on the server.

Preflight

Requests that could change state get checked before being sent. The browser issues an OPTIONS request first:

OPTIONS /data                          ─▶
  Origin: https://shop.example.com
  Access-Control-Request-Method: PUT
  Access-Control-Request-Headers: content-type, authorization

                                       ◀─  204
                                           Access-Control-Allow-Origin: https://shop.example.com
                                           Access-Control-Allow-Methods: GET, POST, PUT
                                           Access-Control-Allow-Headers: content-type, authorization
                                           Access-Control-Max-Age: 86400

then the real PUT is sent

A request avoids preflight only if it’s “simple”: GET, HEAD or POST, with no custom headers, and a Content-Type of application/x-www-form-urlencoded, multipart/form-data or text/plain.

That last condition catches everyone — Content-Type: application/json triggers a preflight. Nearly every real API call is therefore two round trips, not one, which is a latency cost worth knowing about. Access-Control-Max-Age lets the browser cache the preflight result and skip it for subsequent requests.

Credentials

Cookies and authorisation headers are not sent cross-origin by default:

fetch(url, { credentials: 'include' })   // ask for cookies to be sent

The server must then respond with:

Access-Control-Allow-Origin: https://shop.example.com   ← must be exact
Access-Control-Allow-Credentials: true

With credentials, the wildcard is forbidden. Access-Control-Allow-Origin: * is rejected outright when credentials are involved — the server has to name the origin. That’s deliberate: it forces a conscious decision about who may read authenticated data.

The cookie also needs SameSite=None; Secure to be sent cross-site at all — see Cookies.

Reading the errors

MessageMeans
”No ‘Access-Control-Allow-Origin’ header”Server sent no CORS headers. Fix the server
”…does not match”Server named a different origin — often it echoed the wrong one, or you’re on www. and it allows the apex
”…wildcard ’*’ when credentials mode is ‘include‘“Server must name your origin explicitly
”Method PUT is not allowed”Preflight response omits it from Allow-Methods
”Request header field x-foo is not allowed”Preflight response omits it from Allow-Headers
Preflight returns 404 or 405Server doesn’t handle OPTIONS at all — very common on hand-rolled routers

In plain terms: every one of these is the server’s answer, not your request’s fault. No amount of changing the fetch call fixes a missing header — you need the server, or a proxy, or the API’s owner.

Practical

  • You cannot fix CORS from the front end. If you don’t control the API, proxy it through your own origin — which also removes the preflight and keeps any secret server-side
  • Never reflect the Origin header unconditionally into Access-Control-Allow-Origin while allowing credentials. That’s a wildcard with extra steps, and it permits any site to read authenticated responses
  • Set Access-Control-Max-Age so preflights are cached. On a chatty API this halves request count
  • CORS is not authorisation. It controls which origins may read a response, not which users may. Authentication and permission checks still belong on the server
  • Beware Vary: Origin on cached responses — a CDN caching one origin’s allow header and serving it to another produces failures that only appear in production, and only sometimes