What Is CORS?
Cross-Origin Resource Sharing (CORS) is a browser security mechanism that controls which origins (domain + protocol + port) can make requests to your API. Without CORS headers, the browser blocks responses from a different origin — not the request itself, but the response JavaScript can read.
An *origin* is the combination of scheme, host, and port:
https://app.example.com:443. Changing any component creates a different origin, even a port change from 443 to 3000.
Same-Origin Policy
The Same-Origin Policy (SOP) is the browser rule that CORS relaxes. By default:
https://app.example.comcan freely callhttps://app.example.com/apihttps://app.example.comcannot read the response fromhttps://api.example.com(different subdomain)http://localhost:3000cannot read the response fromhttp://localhost:8000(different port)
Note: SOP restricts *reading* responses. Simple POST requests (forms) were always allowed; CORS adds protection for those too.
Simple vs Preflight Requests
Simple Requests
A request is *simple* if it uses GET, HEAD, or POST with specific content types (application/x-www-form-urlencoded, multipart/form-data, text/plain) and no custom headers. Simple requests are sent directly; the browser checks CORS headers on the response.
Preflight Requests
All other requests trigger an automatic preflight — an OPTIONS request sent before the actual request:
OPTIONS /api/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization, Content-Type
The server must respond with appropriate CORS headers before the browser sends the real request:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 3600
Configuring CORS Headers
`Access-Control-Allow-Origin`
The most important header. Options:
# Allow a specific origin
Access-Control-Allow-Origin: https://app.example.com
# Allow any origin (public APIs only)
Access-Control-Allow-Origin: *
Never use * with credentials — it is not permitted. When credentials are involved, you must echo the specific Origin header.
`Access-Control-Allow-Methods`
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
`Access-Control-Allow-Headers`
List all custom headers the client may send:
Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-ID
`Access-Control-Max-Age`
How long (seconds) browsers can cache the preflight result. Reduces preflight overhead for repeat requests:
Access-Control-Max-Age: 86400
Credentials and Cookies
By default, cross-origin requests do not include credentials (cookies, TLS certificates, HTTP auth). To enable:
// Client must opt in
fetch('https://api.example.com/data', { credentials: 'include' })
# Server must respond with:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://app.example.com # must be specific
Common CORS Errors
| Error | Cause | Fix |
|---|---|---|
| Missing CORS header | Server didn't send ACAO | Add CORS middleware |
| Wildcard with credentials | `*` + credentials | Echo Origin explicitly |
| Preflight failing | OPTIONS not handled | Add OPTIONS handler |
| Wrong port in origin | Dev vs prod port | Include all allowed origins |
Security Best Practices
- Whitelist, don't wildcard for authenticated APIs — maintain an explicit list of allowed origins
- Validate the
Originheader server-side — do not blindly echo it; check against your whitelist - Avoid
Access-Control-Allow-Headers: *in production — list headers explicitly - Log rejected CORS requests — suspicious origins are a signal of attempted cross-site attacks
- CORS is a browser enforcement mechanism — it does not protect server-to-server calls or
curlrequests