← Learn··Updated 21 May 2026·3 min read

Understanding CORS

A short reference on Cross-Origin Resource Sharing — what counts as a cross-origin request, the simple vs preflight distinction, the headers that matter, and why CORS is a browser-side restriction rather than a server-side one.

Programming
Web & HTTP
#web
#cors
#security
#http

The one-line definition

CORS — Cross-Origin Resource Sharing — is a browser security mechanism that controls when JavaScript on one origin (scheme + host + port) is allowed to read responses from a different origin. It is enforced entirely by the browser. The server's role is to send headers that tell the browser whether to allow the read.

Same-origin policy comes first

By default, the browser refuses to let JavaScript read responses from anywhere other than the page's own origin. That refusal is the same-origin policy. It is older than CORS and is the default. CORS exists to allow specific, controlled exceptions to it.

An origin is the tuple (scheme, host, port). https://mart.traagel.dev and https://mart.traagel.dev:8080 are different origins. https://mart.traagel.dev and http://mart.traagel.dev are different origins. Subdomains are different origins. CORS treats every one of those differences as cross-origin.

Simple requests vs preflight

The browser splits cross-origin requests into two categories.

Simple requestsGET or POST with only "safelisted" headers and content types (application/x-www-form-urlencoded, multipart/form-data, text/plain). The browser sends the request directly, then inspects the response headers to decide whether the JavaScript caller is allowed to read the body.

Preflighted requests — everything else. The browser sends an OPTIONS request first ("the preflight"), waits for the server to confirm the actual request will be allowed, and only then sends the real request:

sequenceDiagram
    participant JS as Browser JS
    participant B as Browser
    participant S as Server (other origin)
    JS->>B: fetch('https://api.example/users', { method: 'PUT', headers: { 'X-Auth': 'foo' } })
    B->>S: OPTIONS /users<br/>Origin: https://app.example<br/>Access-Control-Request-Method: PUT<br/>Access-Control-Request-Headers: X-Auth
    S-->>B: 204<br/>Access-Control-Allow-Origin: https://app.example<br/>Access-Control-Allow-Methods: PUT<br/>Access-Control-Allow-Headers: X-Auth
    B->>S: PUT /users<br/>Origin: https://app.example
    S-->>B: 200 + payload
    B-->>JS: Promise resolves with response

If the preflight comes back without the right Access-Control-Allow-* headers, the browser refuses to send the real request at all.

The headers that matter

Header Direction What it does
Origin Request Browser tells the server which origin the request is from
Access-Control-Allow-Origin Response Server tells the browser which origin is allowed to read. Either a specific origin or *
Access-Control-Allow-Methods Response Which methods the actual request may use (preflight only)
Access-Control-Allow-Headers Response Which custom headers the actual request may carry (preflight only)
Access-Control-Allow-Credentials Response If true, the browser may include cookies. Allow-Origin cannot be * in this case — it must echo the specific origin
Access-Control-Max-Age Response How long the browser may cache the preflight result. Avoid re-running the OPTIONS dance on every call
Access-Control-Expose-Headers Response Which response headers JS is allowed to read. Default is a tiny safelist

Common misconceptions

  • "CORS protects my API." No. CORS protects the user's browser from a malicious page reading sensitive data from another origin. Anyone running curl against the same endpoint gets the response regardless of CORS headers. If the data needs to be protected, you need authentication and authorisation, not CORS.
  • "Access-Control-Allow-Origin: * is fine." Only if the response is genuinely public and the request does not include credentials. With cookies in play, * is forbidden and the server must echo the specific origin.
  • "I just need to set the header." Preflighted requests need the OPTIONS handler to respond with the right headers and with a 2xx status. A 404 on OPTIONS will block the real request.

Where this hits engineers

The classic CORS bug is a frontend at https://app.example.com calling an API at https://api.example.com and getting blocked. The fix is either:

  • Configure the API to send the right Access-Control-Allow-* headers — usually a one-liner in any modern framework.
  • Put the API behind the same origin via a reverse proxy at /api. Same-origin requests never go through CORS at all.

The reverse proxy route is often the cleaner fix because the same-origin model is simpler than CORS even when it works.

What CORS is not

CORS does not validate the request. It does not authenticate users. It does not filter content. It is one specific check the browser does before letting JavaScript read a cross-origin response. The server, the network, and the auth system are unaffected by CORS — they just answer the request as usual, and CORS happens on the way back.