CORS & Preflight Simulator

Lab

The idea, in plain words

Your browser is a bouncer at the door of every website. Your page lives at app.example.com and wants to call api.example.com — a different building. Before letting your JavaScript read the answer, the bouncer checks the API's guest list. The guest list is a set of HTTP headers the server sends back.

Why it matters: “Blocked by CORS policy” is probably the most common error a new web developer meets. Once you know the bouncer's three questions, you can fix it in minutes.

How to use this lab

  1. 1Pick an example from the list on the left (or the buttons below).
  2. 2Press the big Next step button. Go slowly — one step at a time is the whole point.
  3. 3Read the explanation under the button each time, and watch what lights up.

Stuck? Hit “Full controls” (top right) only when you want to change settings or paste your own input.

What to watch

  1. 1.Is it a different origin? If not, there is no CORS at all.
  2. 2.Does the browser need to ask permission first (a preflight)?
  3. 3.Do the server's reply headers say “yes, this origin may read it”?

Words you'll meet

Origin
— Scheme + host + port, like https://app.example.com.
Preflight
— A quick “may I?” request (OPTIONS) the browser sends before risky requests.
Access-Control-Allow-Origin
— The header where the server names who is allowed to read its response.
Credentials
— Cookies or auth sent along. They make the rules stricter.
Try this:
Easy
Medium
Hard

OPTIONS returns 404Hard

The API has no OPTIONS handler (or auth middleware blocks it), so the preflight dies.

1 · The fetch() call in your page

Page origin
API URLcross origin
Method
Content-Type

Preflight required — the method DELETE is not GET, HEAD or POST.

Step 1 of 7
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: 'DELETE' }.

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: 'DELETE' }.
Headers on this message

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

✗ Blocked at the preflight

Access to fetch at 'https://api.example.com/v1/orders' from origin 'https://app.example.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

The real request was never sent.

How to fix

  • Make the API answer OPTIONS requests with 204 (or 200) and the CORS headers — many frameworks need a CORS middleware registered before auth and routing.
  • Make sure auth middleware doesn't reject OPTIONS: browsers never attach credentials to a preflight.

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

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

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