Asynchronous JS, APIs, and JSON
Fetch and process data asynchronously, design and consume APIs, and add realtime features.
Content
CORS and preflight requests
Versions:
Watch & Learn
AI-discovered learning video
Sign in to watch the learning video for this topic.
CORS and Preflight Requests — Why the Browser Plays Gatekeeper
"The browser is not being annoying — it's protecting your users from someone else's sketchy API."
You're coming off async/await flows and happily parsing JSON responses (nice work!), and now the browser slams the door when you try to fetch data from another origin. Welcome to CORS and preflight requests — the part of web development that sounds like a security feature and feels like a bureaucracy. Let's make it make sense.
What is CORS, in one dramatic sentence?
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that decides whether a web page at origin A is allowed to access resources from origin B. If the server at B doesn't explicitly say "it's okay", the browser blocks the response — not the network request.
Why this matters: without CORS, a malicious page could read private data from another site (cookies, APIs) just because your browser happens to be logged in. So the browser acts like a bouncer asking for ID.
Quick recap connections (building on what you already know)
- You've used fetch with async/await:
const res = await fetch(url); const data = await res.json();— good. - You've parsed JSON and used schemas to validate data shapes — also good.
Now: some fetch calls cause the browser to send a preflight (an extra request) before the real request. That preflight must be answered by the server with the right headers for the real request to proceed.
Simple requests vs. requests that trigger preflight
Browsers allow simple requests without preflight. A request is simple if all of these are true:
- Method is GET, POST, or HEAD
- Allowed Content-Type:
text/plain,multipart/form-data, orapplication/x-www-form-urlencoded - You don't use custom headers (like
X-My-App) or authentication headers likeAuthorization
If any of those are violated (e.g., you POST JSON with Content-Type: application/json, or use PUT, or add a custom header), the browser first sends an OPTIONS request — the preflight.
Imagine it like a club entrance
- Simple request: you walk in, bouncer nods.
- Non-simple: bouncer radios the manager (preflight OPTIONS). Manager checks the guest list (server checks headers). Manager says "OK" (Access-Control-Allow-Origin), or "Nope" (blocked).
Anatomy of the preflight (what actually happens)
- Browser sends an OPTIONS request to the target URL.
- Includes
Originheader (where request is coming from) - Includes
Access-Control-Request-MethodandAccess-Control-Request-Headersdescribing the real request
- Includes
- Server responds to OPTIONS with CORS headers telling the browser what's allowed
- If acceptable, browser proceeds to the real request
- If not, browser blocks the response (your JS never gets access)
Important response headers the server should supply:
Access-Control-Allow-Origin: https://your-site.com(or*in permissive cases)Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONSAccess-Control-Allow-Headers: Content-Type, Authorization, X-My-AppAccess-Control-Allow-Credentials: true(if you need cookies)Access-Control-Max-Age: 600(cache preflight for 10 minutes)
Practical examples
The client (JS) - triggers preflight because we send JSON and a custom header
// triggers preflight because Content-Type is application/json
const res = await fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-My-App': 'cs50-demo'
},
body: JSON.stringify({ name: 'Ada Lovelace' }),
credentials: 'include' // cookies — means server must allow credentials
});
const result = await res.json();
console.log(result);
The server (Express) — answering the preflight
// Node + Express minimal CORS handling
app.options('/data', (req, res) => {
res.set({
'Access-Control-Allow-Origin': 'https://your-frontend.com',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, X-My-App',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Max-Age': '600'
});
return res.sendStatus(204); // no content
});
app.post('/data', (req, res) => {
// normal handling
res.json({ ok: true });
});
Flask example (Python):
from flask import Flask, request, make_response
@app.route('/data', methods=['OPTIONS', 'POST'])
def data():
if request.method == 'OPTIONS':
resp = make_response('', 204)
resp.headers['Access-Control-Allow-Origin'] = 'https://your-frontend.com'
resp.headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS'
resp.headers['Access-Control-Allow-Headers'] = 'Content-Type, X-My-App'
resp.headers['Access-Control-Allow-Credentials'] = 'true'
return resp
# POST handling
return {'ok': True}
Cookies, credentials, and a pitfall to avoid
If your fetch uses credentials: 'include' (or same-origin), the server must:
- Set
Access-Control-Allow-Credentials: true - NOT set
Access-Control-Allow-Origin: *— it must echo the specific origin
If you set * and Allow-Credentials: true, the browser will reject it. The server must return the actual origin string.
Debugging tips (because the console error is your enemy and also your teacher)
- Check browser console: it shows
CORSerrors and often the missing header. - Network tab → look at the OPTIONS request and response to see which header is absent.
- If OPTIONS returns 404 or 500, your server isn't handling preflight — add an OPTIONS handler.
- Use
curl -Ito inspect headers (but remember curl doesn't enforce CORS — only browsers do). Example:
curl -i -X OPTIONS https://api.example.com/data -H "Origin: https://your-frontend.com"
Quick checklist: How to fix CORS problems
- Identify whether a preflight is sent (Network tab).
- Ensure server responds to OPTIONS with the right Access-Control-* headers.
- If using credentials, return a specific origin (not
*) and setAllow-Credentials: true. - If you control client code, avoid unnecessary custom headers/methods or use
application/x-www-form-urlencodedwhen possible — but don’t compromise security. - Use CORS libraries (e.g.,
corsin Express) for sane defaults during development, but configure origins for production.
Key takeaways (so this sticks in your brain like a good meme)
- CORS is a browser-enforced safety policy. Servers must opt into allowing cross-origin access.
- Preflight = OPTIONS request the browser sends when a request is "non-simple".
- Server must reply with Access-Control-Allow- headers* that match the request.
- Use specific origins when credentials are involved — wildcard won't cut it.
Final thought: You've already mastered async/await and JSON parsing — now think of CORS as the handshake step. Your fetch waits politely while the server shows its ID. Once both sides nod, the real conversation begins.
Tags: intermediate, humorous, web development, javascript
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!