CORS Preflight Simulator

A CORS preflight is an automatic OPTIONS request the browser sends before a cross-origin call to ask the server whether the real request is allowed. It fires only for "non-simple" requests, such as a custom header or a PUT. Pick the request shape on the left, configure the server's CORS headers on the right, and watch the preflight appear — or not — and the real request succeed or fail.

preflight
no
final
OK

What you're looking at

The left panel is the request your JavaScript is about to make — method, Content-Type, custom headers, credentials. The right panel is the CORS headers the server answers with. Between them, the simulator shows what the browser decides to do: whether it silently sends an OPTIONS preflight first, and whether the real request is allowed through. The two counters above track exactly that — preflight yes or no, final OK or blocked.

Start with a plain GET and text/plain: no preflight, one request. Now flip the Content-Type to application/json and watch an OPTIONS request appear that your code never asked for. Then break things: switch the method to PUT while Allow-Methods only lists GET and POST, or turn on credentials while Allow-Origin is *. What should surprise you is who does the blocking. The server never refuses anything — it answers happily. The browser is the enforcer, and a "blocked" simple request may still have reached the server and run; the browser just refuses to show your JavaScript the response. That one detail explains most confusing CORS bugs.

Client request
From https://app.example.com
Server CORS response
Browser
fetch(URL, …)
→ simple request (no preflight needed)
Server responds
GET handler runs
Success
Request reached the server and the response is readable by the page.

Why CORS exists

The Same-Origin Policy is the default; CORS is the opt-in.

Without the browser's Same-Origin Policy, any website you visit could send authenticated requests to any other site you happen to be logged in to. Visit a malicious page; it fires a fetch('https://bank.example.com/transfer?to=attacker'); your browser sends your bank cookies along; the bank processes the transfer. The Same-Origin Policy blocks this by refusing to let one origin read responses from another.

CORS (Cross-Origin Resource Sharing) is the opt-in escape hatch. The server says, in its response headers, "yes, this origin is allowed to read my responses". The browser checks those headers and either delivers the response to the page or hides it and shows the famous CORS error.

The preflight exists because some requests would have side effects on the server even if the browser blocked the response. A POST that creates a record happens on the server even if the browser then refuses to read the response. So for "non-simple" requests, the browser fires a preliminary OPTIONS request asking "would you accept this request?" — and only fires the real one if the server says yes.


Four mistakes that cost everyone 30 minutes once

Memorise these and never debug CORS again.

  • Sending JSON triggers a preflight. Content-Type: application/json isn't in the simple-CT list. Switch to text/plain and the preflight disappears — but you usually want JSON, so configure the server to permit it via Access-Control-Allow-Headers: content-type.
  • Wildcard origin and credentials don't mix. The spec forbids Access-Control-Allow-Origin: * together with credentials: 'include'. Pick a specific origin and echo it back from the server based on the request's Origin header.
  • Forgetting Access-Control-Allow-Credentials. Setting credentials: 'include' on the client is half the story; the server must also send Access-Control-Allow-Credentials: true on both the preflight response AND the real-request response.
  • Missing Vary: Origin. If your server echoes the request Origin into Access-Control-Allow-Origin (the common pattern), include Vary: Origin in the response. Without it, a CDN can cache one origin's response and serve it to other origins, breaking the world.
Found this useful?