What is CORS?
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that controls which websites can make API calls to other domains.
What is an Origin?
Origin = Protocol + Domain + Port
https://example.com:443
↑ ↑ ↑
Protocol Domain Port
Same-Origin Examples:
Origin 1: https://example.com/page1
Origin 2: https://example.com/page2
→ Same origin ✓
Cross-Origin Examples:
Origin 1: https://example.com
Origin 2: https://api.example.com
→ Different domain = Cross-origin ✗
Origin 1: https://example.com
Origin 2: http://example.com
→ Different protocol = Cross-origin ✗
Origin 1: https://example.com:443
Origin 2: https://example.com:8080
→ Different port = Cross-origin ✗
Why CORS Exists
Without CORS:
Malicious site: evil.com
↓
JavaScript: fetch('https://bank.com/transfer?to=attacker&amount=1000')
↓
Browser sends request with your bank cookies
↓
Money stolen!
With CORS:
Malicious site: evil.com
↓
JavaScript: fetch('https://bank.com/transfer')
↓
Browser checks: "Does bank.com allow evil.com?"
↓
No CORS headers from bank.com
↓
Browser blocks request ✓
CORS Request Headers (Browser → Server)
1. Origin
Automatically sent by browser on cross-origin requests
Origin: https://mysite.com
Meaning: Tells server which origin is making the request.
Browser adds this automatically - JavaScript cannot modify it.
2. Access-Control-Request-Method
Sent in OPTIONS preflight only
Access-Control-Request-Method: POST
Meaning: Tells server which HTTP method the actual request will use.
3. Access-Control-Request-Headers
Sent in OPTIONS preflight only
Access-Control-Request-Headers: authorization, content-type
Meaning: Tells server which custom headers the actual request will include.
CORS Response Headers (Server → Browser)
1. Access-Control-Allow-Origin
Required for all CORS responses
Access-Control-Allow-Origin: https://trusted-site.com
Meaning: Only trusted-site.com can read this response from JavaScript.
Wildcard (dangerous):
Access-Control-Allow-Origin: *
Meaning: Any website can read this response.
Example:
Request from: https://console.aws.amazon.com
API response includes:
Access-Control-Allow-Origin: https://console.aws.amazon.com
Browser: "Origin matches, allow!" ✓
2. Access-Control-Allow-Methods
Required in OPTIONS preflight response
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Meaning: Browser can use these HTTP methods in cross-origin requests.
3. Access-Control-Allow-Headers
Required in OPTIONS preflight response if custom headers used
Access-Control-Allow-Headers: Authorization, Content-Type, X-Custom-Header
Meaning: Browser can send these headers in cross-origin requests.
Example:
fetch('https://api.example.com', {
headers: {
'Authorization': 'Bearer token123', // Needs Allow-Headers
'X-Custom-Header': 'value' // Needs Allow-Headers
}
})
4. Access-Control-Expose-Headers
Optional - controls which response headers JavaScript can read
Access-Control-Expose-Headers: X-Request-ID, X-Rate-Limit
Meaning: JavaScript can access these headers from the response.
Default readable headers (no Expose-Headers needed):
- Cache-Control
- Content-Language
- Content-Type
- Expires
- Last-Modified
- Pragma
Without this header:
fetch('https://api.example.com')
.then(response => {
console.log(response.headers.get('X-Request-ID')); // null
});
With this header:
fetch('https://api.example.com')
.then(response => {
console.log(response.headers.get('X-Request-ID')); // "abc123"
});
5. Access-Control-Max-Age
Optional - caches preflight response
Access-Control-Max-Age: 86400
Meaning: Browser can cache preflight result for 86400 seconds (24 hours).
6. Access-Control-Allow-Credentials
Required if sending cookies/credentials
Access-Control-Allow-Credentials: true
Meaning: Browser can send cookies and authentication headers.
JavaScript side:
fetch('https://api.example.com', {
credentials: 'include' // Send cookies
})
Cannot use with wildcard:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
→ Invalid! Must specify exact origin
Two Types of CORS Requests
1. Simple Requests (No Preflight)
Conditions:
- Method: GET, HEAD, or POST
- Headers: Only simple headers (Accept, Content-Type, etc.)
- Content-Type:
application/x-www-form-urlencoded,multipart/form-data, ortext/plain
Flow:
Browser sends request directly
↓
GET https://api.example.com/data
↓
Server responds with CORS headers
← Access-Control-Allow-Origin: https://mysite.com
↓
Browser checks origin
↓
If matches: JavaScript can read response ✓
If doesn't match: Browser blocks response ✗
No OPTIONS request needed!
2. Preflighted Requests (Requires OPTIONS)
Conditions (any of these):
- Method: PUT, DELETE, PATCH, CONNECT, TRACE
- Custom headers: Authorization, X-Custom-Header, etc.
- Content-Type:
application/json
Flow:
1. Browser sends OPTIONS (preflight)
→ OPTIONS https://api.example.com/data
→ Access-Control-Request-Method: POST
→ Access-Control-Request-Headers: authorization,content-type
2. Server responds to OPTIONS
← HTTP/1.1 200 OK
← Access-Control-Allow-Origin: https://mysite.com
← Access-Control-Allow-Methods: GET, POST, PUT
← Access-Control-Allow-Headers: authorization,content-type
← Access-Control-Max-Age: 86400
3. Browser checks preflight response
If allowed: Continue to step 4
If not allowed: Block request ✗
4. Browser sends actual request
→ POST https://api.example.com/data
→ Authorization: Bearer token123
→ Content-Type: application/json
5. Server responds
← HTTP/1.1 200 OK
← Access-Control-Allow-Origin: https://mysite.com
← {"result": "success"}
6. Browser checks origin again
If matches: JavaScript can read response ✓
CORS Violation Patterns
Pattern 1: Missing Allow-Origin
Request: fetch('https://api.example.com')
Response: HTTP/1.1 200 OK (no CORS headers)
Browser Console:
Access to fetch at 'https://api.example.com' from origin 'https://mysite.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is
present on the requested resource.
Pattern 2: Origin Mismatch
Request from: https://mysite.com
Response: Access-Control-Allow-Origin: https://othersite.com
Browser Console:
Access to fetch at 'https://api.example.com' from origin 'https://mysite.com'
has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has
a value 'https://othersite.com' that is not equal to the supplied origin.
Pattern 3: Preflight Failure
OPTIONS request fails (403, 404, 500)
Browser Console:
Access to fetch at 'https://api.example.com' from origin 'https://mysite.com'
has been blocked by CORS policy: Response to preflight request doesn't pass
access control check: It does not have HTTP ok status.
Pattern 4: Missing Allow-Headers
Request with: Authorization header
Response: Access-Control-Allow-Headers: content-type (missing authorization)
Browser Console:
Access to fetch at 'https://api.example.com' from origin 'https://mysite.com'
has been blocked by CORS policy: Request header field authorization is not
allowed by Access-Control-Allow-Headers in preflight response.
Pattern 5: Response Visible in HAR but Blocked by JavaScript
Critical scenario: Even when server sends response successfully, browser blocks JavaScript access.
1. OPTIONS preflight succeeds
→ OPTIONS https://api.example.com/data
← HTTP/1.1 200 OK
← Access-Control-Allow-Origin: https://mysite.com
← Access-Control-Allow-Methods: GET, POST
2. Browser sends GET request
→ GET https://api.example.com/data
3. Server responds successfully
← HTTP/1.1 200 OK
← Content-Type: application/json
← {"data": "hello world"}
4. Browser receives response (visible in Network tab/HAR file)
5. Browser checks CORS headers
Missing: Access-Control-Allow-Origin
6. Browser blocks JavaScript access
✓ Response exists in browser memory
✗ JavaScript cannot read it
7. JavaScript sees error
fetch('https://api.example.com/data')
.then(r => r.json())
.catch(e => console.error(e))
// Error: CORS policy: No 'Access-Control-Allow-Origin' header
What you see:
Network tab (F12 → Network):
GET /data HTTP/1.1
Status: 200 OK
Response body: {"data": "hello world"} ← Visible here
HAR file:
{
"response": {
"status": 200,
"content": {
"text": "{\"data\": \"hello world\"}" ← Visible here
}
}
}
JavaScript console:
CORS error: No 'Access-Control-Allow-Origin' header
Cannot read response
Key concept: Browser enforces CORS at the application layer, not network layer.
- Network layer: Response received successfully (200 OK)
- Application layer: JavaScript blocked from reading it (CORS violation)
Both preflight AND actual response need CORS headers:
OPTIONS response must have:
- Access-Control-Allow-Origin
- Access-Control-Allow-Methods
- Access-Control-Allow-Headers
GET/POST response must also have:
- Access-Control-Allow-Origin ← Often forgotten!
CORS with Status 200
Important: CORS violations can happen even when HTTP returns 200 OK.
1. Browser sends request
2. Server responds: HTTP/1.1 200 OK (no CORS headers)
3. Browser receives response successfully
4. Browser checks CORS policy
5. Missing Access-Control-Allow-Origin
6. Browser blocks JavaScript from reading response
7. JavaScript sees: Error (even though HTTP was 200)
Network layer: Success (200 OK) Browser security layer: Blocked (CORS violation)
Real-World Example: AWS Console
Scenario:
AWS Console (https://console.aws.amazon.com) calls AWS API (https://api.aws.amazon.com)
Without CORS headers:
Console JavaScript:
fetch('https://api.aws.amazon.com/data')
Browser: "Cross-origin request!"
Browser: "No Access-Control-Allow-Origin header"
Browser: "BLOCKED"
Console: Error - Cannot access API
With CORS headers:
API Response:
Access-Control-Allow-Origin: https://console.aws.amazon.com
Access-Control-Allow-Headers: Authorization, X-Amz-Date
Access-Control-Expose-Headers: X-Amzn-Requestid
Browser: "Origin matches, headers allowed"
Browser: "Allow access"
Console: Success - Can read response ✓
Corporate Proxy and CORS
Problem: Proxy strips CORS headers
Browser → Proxy → AWS API
← 200 OK ← (with CORS headers)
Proxy strips headers:
- Removes Access-Control-Allow-Origin
- Removes Access-Control-Allow-Headers
Browser ← 200 OK (no CORS headers)
Browser: "CORS violation - blocked!"
Result: Status 200 but ERR_FAILED in browser.
Debugging CORS Issues
1. Check Browser Console
F12 → Console tab
Look for: "blocked by CORS policy"
2. Check Network Tab
F12 → Network tab → Click request
Headers tab:
- Check Response Headers for Access-Control-*
- Check if OPTIONS request exists
- Check OPTIONS response status
3. Test with curl (Bypasses CORS)
curl -v https://api.example.com
# If curl succeeds but browser fails
# → CORS issue confirmed
4. Check Preflight
curl -X OPTIONS https://api.example.com \
-H "Origin: https://mysite.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: authorization"
# Should return:
# Access-Control-Allow-Origin: https://mysite.com
# Access-Control-Allow-Methods: POST
# Access-Control-Allow-Headers: authorization
Technical Terms
- CORS: Cross-Origin Resource Sharing - browser security mechanism
- Origin: Protocol + Domain + Port combination
- Same-Origin Policy: Browser blocks cross-origin requests by default
- Preflight request: OPTIONS request sent before actual request
- Simple request: Request that doesn’t require preflight
- Preflighted request: Request that requires OPTIONS preflight
- Access-Control-Allow-Origin: Header specifying allowed origins
- Access-Control-Allow-Headers: Header specifying allowed request headers
- Access-Control-Expose-Headers: Header specifying readable response headers
- Access-Control-Max-Age: How long to cache preflight response
Common Mistakes
1. Wildcard with Credentials
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
→ Invalid! Cannot use * with credentials
2. Multiple Origins
Access-Control-Allow-Origin: https://site1.com, https://site2.com
→ Invalid! Only one origin allowed
→ Solution: Server checks origin and returns matching one
3. Missing Preflight Headers
Server handles POST but not OPTIONS
→ Preflight fails
→ Actual POST never sent
Notes
- CORS is enforced by browsers, not servers
- curl and server-to-server requests bypass CORS
- CORS headers must be in every response, not just OPTIONS
- Preflight responses can be cached (Access-Control-Max-Age)
- Status 200 doesn’t mean CORS succeeded - check headers
- Corporate proxies can strip CORS headers, causing failures
- Always check browser console for CORS error messages