Handling CORS
1. Understanding Cross-Origin Requests
Browsers block JS from reading responses across origins (different scheme/host/port) unless server opts in via CORS headers.
| Same Origin | Cross Origin |
https://app.com → https://app.com/api | https://app.com → https://api.com |
| No CORS needed | Server must send CORS headers |
2. Configuring Access-Control-Allow-Origin
| Value | Effect |
* | Any origin (no credentials allowed) |
https://app.example.com | Single specific origin |
| Echo request Origin (validated) | Multi-origin support; must validate against allowlist |
null | Avoid — file:// and sandboxed iframes |
3. Configuring Access-Control-Allow-Methods
| Header | Value |
| Allow common methods | GET, POST, PUT, PATCH, DELETE, OPTIONS |
| Per-endpoint specific | Only methods that endpoint supports |
| Required | For preflight responses |
| Header | Notes |
| List custom headers | Content-Type, Authorization, X-Request-ID |
| Echo request | Reflect Access-Control-Request-Headers (validated) |
Wildcard * | Allowed (without credentials) |
5. Handling Preflight Requests
CORS Preflight Flow
- Browser detects "non-simple" request (custom header, PUT/DELETE, JSON body, etc.)
- Sends OPTIONS request with
Access-Control-Request-Method + ...-Headers
- Server responds with
Access-Control-Allow-* headers (no body, 204)
- Browser caches preflight per
Access-Control-Max-Age
- Browser sends actual request
6. Using Access-Control-Max-Age
| Value | Effect |
600 (10 min) | Common default |
86400 (24h) | Max in Chrome (effective cap: 7200 / 2h) |
| Higher = fewer preflights | Tradeoff: slower revocation of policy changes |
7. Implementing Access-Control-Allow-Credentials
| Setup | Requirements |
| Send cookies / Authorization | Server: Allow-Credentials: true |
| Origin restriction | Cannot use * with credentials |
| Allow-Headers / Methods | Cannot use * |
| Client | Fetch: credentials: "include" |
8. Handling Simple vs Preflighted Requests
| Simple Request Criteria | Anything Else → Preflight |
| Method: GET, HEAD, POST | PUT, PATCH, DELETE, etc. |
| Content-Type: form-urlencoded, multipart/form-data, text/plain | application/json triggers preflight |
| Only CORS-safelisted headers | Custom headers (X-*, Authorization) trigger preflight |
9. Configuring CORS for Multiple Origins
Example: Validated Origin Echo
Set<String> allowed = Set.of(
"https://app.example.com",
"https://admin.example.com"
);
String origin = req.getHeader("Origin");
if (origin != null && allowed.contains(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Vary", "Origin");
}
Warning: Always include Vary: Origin when echoing origin to prevent cache poisoning.
10. Debugging CORS Issues
| Symptom | Cause |
| "No 'Access-Control-Allow-Origin' header" | Server not sending CORS headers |
| "Credentials flag is true, but Allow-Origin is *" | Use specific origin |
| Preflight 4xx/5xx | OPTIONS handler missing or auth-protected |
| Headers not visible to JS | Add to Access-Control-Expose-Headers |