CORS & Preflight Simulator

Lab
Easy
Medium
Hard

JSON POST needs a preflightMedium

Content-Type: application/json is not “simple”, so the browser sends an OPTIONS first.

1 · The fetch() call in your page

Page origin
API URLcross origin
Method
Content-Type

Preflight required — Content-Type: application/json is not one of the three “simple” types.

1 / 10
Decide
fetch('https://api.example.com/v1/orders'). Code running on https://app.example.com asks the browser to call https://api.example.com. Options: { method: 'POST', headers: { 'Content-Type': 'application/json' } }.

Browser ↔ server conversation

DecidePreflightRequestResult
Your JS
page script
Browser
enforces CORS
API server
sends CORS headers
fetch('https://api.example.com/v1/orders')
Code running on https://app.example.com asks the browser to call https://api.example.com. Options: { method: 'POST', headers: { 'Content-Type': 'application/json' } }.
Headers on this message

No headers on this step — it happens inside the browser.

✓ Allowed — after a preflight

Everything the browser needs to hand the response to your JavaScript is in place.

Learn the theory

CORS & Preflight Simulator: how it works

5 min read · Updated

CORS (Cross-Origin Resource Sharing) is the mechanism that lets a server tell the browser: “yes, JavaScript from that origin may read my responses.” It exists because browsers enforce the same-origin policy — and it is the source of the most-searched error in front-end development. Once you know the three questions the browser asks (is it cross-origin? does it need a preflight? do the response headers opt in?), every CORS error becomes diagnosable in seconds.

In short

  • An origin is scheme + host + port. https://app.example.com and https://api.example.com are different origins.
  • CORS is enforced by the browser, not the server. curl and server-to-server calls ignore it.
  • “Non-simple” requests (JSON bodies, Authorization, PUT/DELETE…) trigger a preflight OPTIONS request first.
  • The server opts in with Access-Control-Allow-* response headers. No header, no access to the response.
  • A blocked request can still reach and change the server — CORS hides the response, it doesn't stop the request.

Same-origin policy and what an origin is

Without the same-origin policy, any website you open could use your logged-in session to read your email or bank balance by quietly calling those sites from JavaScript. So browsers refuse to let a page read responses from a different origin unless that origin agrees.

URLSame origin as https://app.example.com?Why
https://app.example.com/page2YesOnly the path differs
http://app.example.comNoDifferent scheme (http vs https)
https://api.example.comNoDifferent host
https://app.example.com:8443NoDifferent port

What CORS is — and what it isn't

CORS is a controlled relaxation of the same-origin policy. The browser attaches an Origin header to cross-origin requests; the server answers with Access-Control-Allow-Origin (and friends) to say who may read the result.

It also doesn't apply to everything: <img>, <script src> and form posts have always been allowed cross-origin. CORS governs fetch(), XMLHttpRequest, web fonts and a few other APIs that expose the response to JavaScript.

Simple requests vs preflighted requests

A request is “simple” — sent immediately with no permission check — only if all of these are true:

  • The method is GET, HEAD or POST.
  • The only headers are CORS-safelisted ones (Accept, Accept-Language, Content-Language, Content-Type, …).
  • If Content-Type is set, it is text/plain, application/x-www-form-urlencoded or multipart/form-data.

Anything else — PUT, DELETE, PATCH, Content-Type: application/json, an Authorization header or a custom header — needs a preflight: an automatic OPTIONS request asking the server for permission before the real request is sent.

OPTIONS /v1/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Max-Age: 86400

Access-Control-Max-Age tells the browser how long it may cache this answer, so repeat calls skip the extra round trip (browsers cap it: roughly 2 hours in Chrome, 24 hours in Firefox).

The headers that matter

HeaderDirectionPurpose
OriginRequestWho is asking. Added by the browser; scripts can't change it.
Access-Control-Request-Method / -HeadersPreflight requestDescribes the real request that will follow.
Access-Control-Allow-OriginResponseThe origin allowed to read the response — an exact origin or *.
Access-Control-Allow-MethodsPreflight responseMethods the server accepts cross-origin.
Access-Control-Allow-HeadersPreflight responseRequest headers the server accepts.
Access-Control-Allow-CredentialsResponsetrue lets the page send and receive cookies/HTTP auth.
Access-Control-Max-AgePreflight responseHow long the preflight result may be cached.
Vary: OriginResponseTells caches the response differs per Origin — required when you echo the origin.

Cookies and credentials

By default cross-origin fetch() sends no cookies. Opting in with credentials: 'include' triggers stricter rules, and each one is a common source of errors:

  • Access-Control-Allow-Origin *cannot be `**. It must echo the exact origin
  • The response must include Access-Control-Allow-Credentials: true (on the preflight response too).
  • * in Allow-Headers or Allow-Methods is treated as a literal string, not a wildcard.
  • The wildcard in Allow-Headers never covers Authorization — list it explicitly.

Common errors and how to fix them

Console messageCauseFix
No 'Access-Control-Allow-Origin' header is presentThe response has no CORS header (or the OPTIONS handler returned an error page).Send Access-Control-Allow-Origin on every response, including errors and OPTIONS.
…must not be the wildcard '*' when the request's credentials mode is 'include'Cookies requested but the server answered *.Echo the exact origin and add Vary: Origin.
Request header field authorization is not allowed by Access-Control-Allow-HeadersThe preflight response didn't list the header.Add it to Access-Control-Allow-Headers.
Method PUT is not allowed by Access-Control-Allow-MethodsMethod missing from the preflight response.Add it to Access-Control-Allow-Methods.
Response to preflight request doesn't pass access control check: It does not have HTTP ok statusThe API returned 404/401/405 for OPTIONS.Handle OPTIONS before auth and routing, return 204.

Configuring a server

Most frameworks ship CORS middleware. Register it before authentication so preflights aren't rejected:

// Express
import cors from 'cors';

app.use(cors({
  origin: ['https://app.example.com'],   // allow-list, echoed back
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['content-type', 'authorization'],
  credentials: true,
  maxAge: 86400,
}));
# Nginx
location /api/ {
  if ($request_method = OPTIONS) {
    add_header Access-Control-Allow-Origin  "https://app.example.com";
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
    add_header Access-Control-Allow-Headers "content-type, authorization";
    add_header Access-Control-Max-Age 86400;
    return 204;
  }
}

Frequently asked questions

Why do I get a CORS error in the browser but not in Postman or curl?
CORS is enforced by browsers. Postman and curl don't run the same-origin policy, so they send the request and show you the response regardless of CORS headers.
Does a CORS error mean the request wasn't sent?
Not necessarily. For simple requests the server receives and processes the request; the browser only withholds the response from your JavaScript. Only a failed preflight stops the real request from being sent.
What is a preflight request?
An automatic OPTIONS request the browser sends before a non-simple cross-origin request, asking the server which origin, method and headers it accepts.
Can I use Access-Control-Allow-Origin: * with cookies?
No. When credentials are included the browser rejects the wildcard. Respond with the exact origin and set Access-Control-Allow-Credentials: true.
How do I avoid CORS entirely in development?
Serve the API from the same origin through a dev-server proxy (Vite, webpack, Next.js rewrites). The browser then sees same-origin requests. Don't disable web security in the browser.
What does mode: 'no-cors' do?
It lets you send a simple cross-origin request but gives you an opaque response you cannot read. It's useful for fire-and-forget calls such as analytics beacons, not for reading data.

Keep exploring

More interactive labs

All labs

HTTP Caching Explained

Cache-Control, ETag and 304s, one request at a time.

Networking12 min · Intermediate

What Happens When You Type a URL

DNS, TCP, TLS and HTTP — with a stopwatch.

Networking10 min · Beginner

JS Event Loop Visualizer

See the call stack, microtasks and timers run, frame by frame.

JavaScript10 min · Intermediate

How Database Indexes Work (B-Tree)

See a B-tree lookup beat a full table scan, and why some queries ignore your index.

Databases12 min · Intermediate

What Happens When 100 Reads Hit the Database

Connection pools, CPU workers, caches and replicas, as 100 requests arrive at once.

Databases12 min · Intermediate

SQL Isolation Levels Lab

Run two transactions side by side and watch dirty reads, phantoms and write skew appear.

Databases15 min · Advanced

Sharding & Replication

Split data across servers, copy it for reads, and see what each choice costs.

Databases15 min · Advanced

Redis Commands & Data Structures

Strings, lists, hashes, sets and sorted sets, one command at a time.

Redis15 min · Beginner

Redis Caching: Cache-Aside, TTL & Eviction

Hit or miss? Watch LRU, LFU, TTLs and a cache stampede decide your database load.

Redis12 min · Intermediate

Webhooks: Delivery, Retries & Signatures

Why you get duplicate deliveries, lost events and forged requests — and how to stop them.

Queues & Jobs12 min · Intermediate

Cron Jobs: Schedules, Overlaps & Missed Runs

Write a cron expression and watch what really happens when jobs overlap or the scheduler is down.

Queues & Jobs10 min · Beginner

Job Queues with BullMQ (Redis)

Watch jobs move through waiting, active, delayed, completed and failed — and see what BullMQ stores in Redis.

Queues & Jobs14 min · Intermediate

RabbitMQ: Exchanges, Queues, Acks & Dead Letters

Direct, topic and fanout routing, competing consumers, prefetch, redelivery and dead-letter queues, drawn.

Queues & Jobs14 min · Intermediate

Git Commands, HEAD & Refs (Visual)

Every core git command drawn with arrows: three areas, commits, branches, HEAD and remotes.

Git15 min · Beginner

Git Merge vs Rebase

Watch the commit graph change under merge, rebase and squash.

Git10 min · Beginner