CORS Preflight in Practice: Credentials, Simple Requests, Misconfigurations
CORS allows a server to tell the browser which other origins are allowed to read its responses. For some cross-origin requests, the browser first performs a preflight request using OPTIONS to ask whether the actual request is allowed. The most common real-world failures happen when the frontend sends credentials or custom headers, but the server's CORS response does not match what the browser expects.
Quick Navigation: CORS Mental Model ⢠Simple Requests vs Preflighted Requests ⢠Preflight Request Flow ⢠Credentials Make CORS Stricter ⢠Important CORS Headers ⢠Common Misconfigurations ⢠Debugging Mindset ⢠Interview Scenarios
Quick Decision Guide
Decision Guide:
- Start with the question: is this request simple or preflighted? - Preflight means the browser sends an OPTIONS request first. - The browser checks origin, method, and requested headers before sending the real request. - Credentialed CORS is stricter: the server must explicitly allow credentials, and Access-Control-Allow-Origin: * does not work with credentials. - Many CORS bugs are not frontend code bugs. They are policy mismatches between browser, API server, proxy, and CDN.
Interview framing: CORS is a browser-enforced access control layer on top of HTTP. Debugging it means reasoning across the request flow, not just reading one frontend error message.
CORS Mental Model
CORS is not a server-side security feature by itself.
It is a browser enforcement mechanism for cross-origin reads.
That means:
200 OKSame-origin refresher
Two URLs are considered different origins if they differ in scheme, host, or port.
Examples:
https://app.example.com ā different from http://app.example.comhttps://app.example.com ā different from https://api.example.comhttps://app.example.com:443 ā different from https://app.example.com:8443High-level flow
Frontend code on Origin A
|
| fetch()/XHR
v
Browser checks CORS rules
|
|-- simple request -> send actual request directly
|
|-- non-simple request -> send preflight OPTIONS first
|
v
Server approves or denies
|
v
Browser decides whether JS can read responseSimple Requests vs Preflighted Requests
A request is commonly treated as simple only if it stays within a narrow safe set.
Usually simple
GET, HEAD, or POSTContent-Type, if set, stays within: - application/x-www-form-urlencoded
- multipart/form-data
- text/plain
Common preflight triggers
PUT, PATCH, or DELETEAuthorization or X-*Content-Type: application/jsonInterview trap
A POST request is not automatically simple.
For example:
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ok: true })
});This often triggers preflight because application/json is not one of the simple content types.
Quick classifier
GET / HEAD / POST
+ no custom headers
+ simple content-type
=> likely simple
PUT / PATCH / DELETE
OR Authorization / X-Custom-Header
OR Content-Type: application/json
=> preflight likelyPreflight Request Flow
When the browser decides a request is non-simple, it first sends an OPTIONS request.
The goal is to ask the server:
Browser-driven preflight
1. Browser wants to send:
POST /orders
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer ...
2. Browser first sends:
OPTIONS /orders
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
3. Server responds with policy:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
4. If browser accepts the policy, it sends the real POST request.Preflight flow diagram
āāāāāāāāāāāāāāāā
ā Frontend JS ā
āāāāāāāā¬āāāāāāāā
ā fetch(...)
v
āāāāāāāāāāāāāāāā
ā Browser ā
ā CORS engine ā
āāāāāāāā¬āāāāāāāā
ā OPTIONS preflight
v
āāāāāāāāāāāāāāāā
ā API / Proxy ā
āāāāāāāā¬āāāāāāāā
ā Access-Control-Allow-*
v
āāāāāāāāāāāāāāāā
ā Browser ā
ā validates ā
āāāāāāāā¬āāāāāāāā
ā allowed
v
āāāāāāāāāāāāāāāā
ā Actual call ā
ā POST/PUT/... ā
āāāāāāāāāāāāāāāāImportant note
Preflight is about permission to send the actual request.
It does not guarantee the actual request's final response will be readable. The actual response must also satisfy the needed CORS rules.
Credentials Make CORS Stricter
Credentialed cross-origin requests include things like:
Frontend example
fetch('https://api.example.com/me', {
credentials: 'include'
});Server requirements
For credentialed requests, the server must be explicit:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: trueCritical rule
This does not work:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: trueIf credentials are involved, wildcard origin is invalid for exposing the response.
Practical mental model
No credentials:
ACAO can sometimes be *
With credentials:
ACAO must be a specific allowed origin
ACAC must be trueAnother real-world gotcha
Even when server and client look correct, third-party cookie policies can still block cookie-based flows. So credentialed CORS can fail because of browser cookie policy, not only because of missing CORS headers.
Important CORS Headers
Request headers sent by the browser
#### Origin
Sent on cross-origin requests so the server knows who is asking.
#### Access-Control-Request-Method
Sent on preflight to tell the server which actual method the browser wants to use.
#### Access-Control-Request-Headers
Sent on preflight to tell the server which non-safelisted headers will be used.
Response headers from the server
#### Access-Control-Allow-Origin
Which origin is allowed to read the response.
#### Access-Control-Allow-Methods
Which methods are allowed for the actual request after preflight.
#### Access-Control-Allow-Headers
Which request headers are allowed for the actual request after preflight.
#### Access-Control-Allow-Credentials
Whether the browser may expose the response when credentials are included.
#### Access-Control-Max-Age
How long the preflight result may be cached by the browser.
#### Access-Control-Expose-Headers
Which response headers JavaScript is allowed to read beyond the default exposed set.
Production nuance: `Vary: Origin`
If your server dynamically echoes back an allowed origin instead of using *, you usually also want:
Vary: OriginThat helps caches understand that the response can differ by request origin.
Common Misconfigurations
1) `application/json` unexpectedly triggers preflight
Teams assume a POST is simple, but JSON content type often makes it preflighted.
2) Missing one header in `Access-Control-Allow-Headers`
The frontend sends:
Authorization, Content-TypeBut the server only allows:
Access-Control-Allow-Headers: Content-TypeResult: preflight fails.
3) Credentials enabled on client, not allowed on server
Client:
fetch(url, { credentials: 'include' })Server forgets:
Access-Control-Allow-Credentials: trueResult: browser blocks response exposure.
4) Wildcard origin with credentials
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: trueLooks permissive, but browser rejects it for credentialed access.
5) Preflight passes but actual response still fails
Some teams only add CORS headers on OPTIONS, not on the actual GET/POST/PUT response.
The browser may allow the send, then still block JS from reading the final response.
6) Proxy/CDN rewrites headers
Your backend is correct, but an edge layer strips Vary: Origin or overwrites Access-Control-Allow-* headers.
7) Redirects after preflight
Cross-origin redirects after a preflighted request can still fail in some browser situations and are a painful debugging case.
Debugging Mindset
When debugging CORS, do not stop at the frontend error overlay.
Open DevTools and inspect the network flow.
What to check
1. What is the page origin?
2. Did the browser send a preflight OPTIONS request?
3. What did preflight send in:
- Origin
- Access-Control-Request-Method
- Access-Control-Request-Headers
4. What did the preflight response return in:
- Access-Control-Allow-Origin
- Access-Control-Allow-Methods
- Access-Control-Allow-Headers
- Access-Control-Allow-Credentials
5. Did the actual response also include the needed CORS headers?
6. Are credentials involved?
7. Is a proxy, CDN, gateway, or nginx layer changing headers?
Practical debugging flow
Frontend error says: blocked by CORS
|
v
Check Network tab
|
|-- no OPTIONS request?
| likely simple request or blocked before preflight scenario
|
|-- OPTIONS exists?
inspect request + response headers
confirm exact method + exact requested headers
confirm origin handling
confirm credentials rules
|
v
Then inspect actual request response
|
v
Then inspect proxy/CDN/server config mismatchSenior-level insight
CORS debugging is often about finding the layer that owns the policy:
Interview Scenarios
Scenario 1: Why does `GET` work but `PUT` fail?
Because PUT commonly triggers preflight, while the GET may be simple enough to skip it.
Scenario 2: Why does Postman work but browser fails?
Because CORS is a browser enforcement mechanism. Tools like Postman are not enforcing browser CORS policy the same way.
Scenario 3: Why does localhost work but production fails?
Because local dev often uses a same-origin dev proxy, while production becomes truly cross-origin.
Scenario 4: Why does server return `200` but frontend still errors?
Because the browser received the response but refused to expose it to JavaScript.
Scenario 5: Why are cookies not being sent?
Possible reasons:
credentials: 'include'Access-Control-Allow-Credentials: true