Guide
7 min read

CORS explained: same-origin policy, preflight requests, and response headers

CORS is one of the most common sources of confusion in web development. This guide explains why browsers enforce it, what preflight requests are, and exactly which headers to set to make it work.

URL Parser

Inspect request URLs and query parameters

Open tool

What is CORS and why do browsers enforce it?

Cross-Origin Resource Sharing (CORS) is a browser mechanism that controls which cross-origin requests are allowed. It exists because of the same-origin policy: a security rule that prevents JavaScript on one origin from reading responses from a different origin.

The same-origin policy protects users. Without it, a malicious page at evil.com could silently make requests to your-bank.com using your cookies, read the response, and exfiltrate your data.

CORS is the controlled relaxation of that policy. It lets servers declare which outside origins they trust by adding specific HTTP response headers. The browser reads those headers and either allows or blocks the cross-origin response.

What counts as "same origin"?

Two URLs share an origin only if all three of these match exactly:

PartExampleCross-origin if different?
Schemehttps vs httpYes
Hostapi.example.com vs example.comYes
Port:3000 vs :8080Yes

A common surprise: http://localhost:3000 and http://localhost:8080 are different origins because the port differs. This is why your frontend dev server often hits CORS errors when calling a local API on a different port.

Simple requests vs. preflight requests

Not all cross-origin requests trigger a preflight. The browser sorts them into two categories:

AspectSimple requestPreflighted request
Methods allowedGET, POST, HEADAny method (PUT, DELETE, PATCH, etc.)
Content-Typeapplication/x-www-form-urlencoded, multipart/form-data, text/plainapplication/json and any other type
Custom headersNone beyond CORS-safelisted headersAny custom header (e.g. Authorization)
Browser sends OPTIONS firstNoYes
Extra round-tripNoYes (cached by Access-Control-Max-Age)

For a preflighted request, the browser automatically sends an HTTP OPTIONS request to the same URL before the actual request. If the server's preflight response does not include the right CORS headers, the browser aborts the real request entirely.

Most modern API calls (JSON POST with Authorization header) are preflighted. If you see an OPTIONS request in your network tab followed by a CORS error, the preflight is failing.

CORS response headers reference

Access-Control-Allow-Origin

Which origins are allowed. Use a specific origin (https://example.com) or * for public APIs. Cannot be * when credentials are included.

Access-Control-Allow-Methods

Which HTTP methods are permitted for the cross-origin request, e.g. GET, POST, PUT, DELETE.

Access-Control-Allow-Headers

Which request headers the client is allowed to send, e.g. Content-Type, Authorization.

Access-Control-Allow-Credentials

Set to true to allow cookies and authorization headers. Requires a specific origin, not *.

Access-Control-Max-Age

How long (in seconds) the browser can cache the preflight result. Reduces round-trips. Common value: 86400 (one day).

Common CORS errors and how to fix them

No Access-Control-Allow-Origin header

Cause: The server did not include CORS headers in its response.

Fix: Add Access-Control-Allow-Origin to the response on the server. The fix is always server-side.

Credential flag is true but Access-Control-Allow-Origin is *

Cause: You set withCredentials: true on the request, but the server responds with * as the allowed origin.

Fix: Replace * with the specific requesting origin (e.g. https://app.example.com). Wildcards and credentials are mutually exclusive.

Request header not in Access-Control-Allow-Headers

Cause: Your request sends a custom header (e.g. Authorization or X-Api-Key) that the server has not whitelisted.

Fix: Add the header name to Access-Control-Allow-Headers in the preflight response.

Method not in Access-Control-Allow-Methods

Cause: Your request uses a method (e.g. DELETE) that the server has not listed as allowed.

Fix: Add the method to Access-Control-Allow-Methods in the preflight response.

Configuring CORS in Express / Node

The fix for a CORS error is always on the server. Here is a minimal Express middleware that handles both preflight and regular requests:

const express = require('express')
const app = express()

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com')
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
  res.setHeader('Access-Control-Max-Age', '86400')

  if (req.method === 'OPTIONS') {
    return res.sendStatus(204)
  }
  next()
})

For production, use the cors npm package and configure it with a whitelist of allowed origins rather than a static string. Never use * for authenticated endpoints.

Frequently asked questions

Why can't I just disable CORS in my browser?

You can, with browser flags or extensions, but only for your own browser. Your users cannot. The same-origin policy exists to protect them from malicious pages making requests on their behalf (CSRF-style attacks). Disabling it for development is acceptable, but the real fix is always configuring the server correctly.

Does CORS protect the server?

Not really. CORS is enforced by the browser, not the server. Tools like curl or Postman bypass it entirely because they are not browsers. CORS protects users from malicious websites making cross-origin requests using the user's credentials. Actual server security still requires authentication, authorization, and rate limiting.

What is a CORS proxy and should I use one?

A CORS proxy sits between your browser and the target server, adding CORS headers to the response. It works for third-party APIs you cannot configure directly. For your own APIs, always configure the server directly. Public CORS proxies should never be used with authenticated requests since all traffic passes through them.

Why does my API work in Postman but not the browser?

Postman is not a browser and does not enforce the same-origin policy. It sends requests directly without preflight checks or CORS validation. If your API works in Postman but fails in the browser, the API is reachable and responding correctly. The problem is missing CORS headers on the server response.

Can I use Access-Control-Allow-Origin: * with credentials?

No. The spec explicitly forbids it. When a request includes credentials (cookies, HTTP auth, or client certificates), the server must respond with a specific origin, not a wildcard. If you set both, the browser will reject the response even though the server sent it.

What does Access-Control-Max-Age do?

It tells the browser how long to cache the preflight result in seconds. Without it, the browser sends an OPTIONS preflight before every non-simple cross-origin request. Setting a long max-age (e.g. 86400 for one day) eliminates these extra round-trips and noticeably speeds up APIs that receive many cross-origin requests.

Need to inspect a URL or parse query parameters?

Use the URL Parser to break apart any URL into its components instantly.

Open URL Parser