Debug Playbook: CORS Preflight Failure (Browser vs. cURL)
Symptom: Browser-initiated OPTIONS preflight request fails with a CORS error, but equivalent curl requests to the same endpoint succeed. Subsequent actual data requests are blocked.
Hypothesis List:
- Credential Handling Discrepancy: The browser might be sending or expecting credentials (
withCredentials: true) differently than cURL, or the server is not correctly configured to handle Access-Control-Allow-Credentials. - Missing or Mismatched `Access-Control-Allow-Headers`: The browser's actual request headers (beyond simple headers) are not fully whitelisted in the server's
Access-Control-Allow-Headers response for the OPTIONS request. - HTTP Method Mismatch for Preflight: The server is not correctly responding to the
OPTIONS method with the expected Access-Control-Allow-Methods header that includes the actual request method (e.g., POST). - Browser Caching of Preflight Responses: The browser might be caching an erroneous or outdated
OPTIONS response, especially if Access-Control-Max-Age is set incorrectly or too high. - Redirects or Intermediary Proxies: A redirect or an intermediary proxy might be stripping or modifying CORS-related headers, specifically for
OPTIONS requests, before they reach the intended server. - SSL/TLS Issues (Less Common for Preflight, but Possible): Mismatches in SSL certificates or trust chains between the browser and the server, although typically manifesting earlier, can sometimes interfere.
Checks (with commands/steps):
- Inspect Browser Network Tab for `OPTIONS` Request/Response:
* Open Developer Tools (F12) -> Network tab. * Filter by OPTIONS method. * Examine Request Headers: Note Origin, Access-Control-Request-Method, Access-Control-Request-Headers. * Examine Response Headers (if any): Look for Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Credentials, Access-Control-Max-Age. * Check Status Code: A 200 OK is expected for a successful preflight. Non-200 codes (e.g., 401, 403, 500) indicate server-side issues.
- Verify `curl` Behavior with Detailed Headers:
* Replicate the browser's OPTIONS request with curl to ensure it truly succeeds: curl -v -X OPTIONS -H "Origin: <your-frontend-origin>" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: Content-Type, Authorization" {{request_url}} * Compare curl's response headers to the browser's expected headers. Look for Access-Control-* headers.
- Server-Side CORS Configuration Review:
* Examine the backend code for CORS middleware or configuration (e.g., Express cors package, Spring Boot WebMvcConfigurer). * Confirm Access-Control-Allow-Origin includes the frontend origin. * Confirm Access-Control-Allow-Methods includes OPTIONS and the method of your actual request (e.g., POST). * Confirm Access-Control-Allow-Headers explicitly lists all non-simple headers sent by the browser (e.g., Content-Type, Authorization, X-Custom-Header). * Verify Access-Control-Allow-Credentials is set to true if your frontend uses withCredentials: true. If so, Access-Control-Allow-Origin *cannot* be *. * Check Access-Control-Max-Age setting.
- Clear Browser Cache and Site Data:
* In Chrome: Developer Tools -> Application tab -> Storage -> Clear site data. * In Firefox: Settings -> Privacy & Security -> Cookies and Site Data -> Clear Data. * Test again after clearing.
- Check for Redirects:
* Use curl -v {{request_url}} to see if any redirects occur before the final endpoint. * Ensure any proxies or load balancers are configured to pass CORS headers correctly.
Likely Fixes:
- Credential Mismatch:
* If frontend uses withCredentials: true, ensure backend sets Access-Control-Allow-Credentials: true and Access-Control-Allow-Origin is a specific origin, not *. * If credentials are not needed, remove withCredentials: true from frontend fetch/XHR calls.
- Missing Headers:
* Add all necessary headers (e.g., Authorization, Content-Type, custom headers) to Access-Control-Allow-Headers in your backend CORS configuration. * Example for Express: app.use(cors({ origin: 'your-frontend-origin', methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] }));
- Method Not Allowed:
* Ensure OPTIONS and your actual request method (e.g., POST) are explicitly listed in Access-Control-Allow-Methods on the server.
- Stale Preflight Cache:
* Reduce Access-Control-Max-Age during development, or remove it temporarily to disable preflight caching. * Ensure Access-Control-Max-Age is not set to a negative value or an excessively long duration in production if issues persist.
- Proxy/Redirect Interference:
* Configure proxies/load balancers to preserve Origin, Access-Control-Request-* headers on incoming requests and Access-Control-Allow-* headers on responses. * Ensure any redirects are handled gracefully and don't strip headers.
Verification:
- After applying a fix, clear browser cache and retry the browser-based request.
- Confirm the
OPTIONS request in the browser's network tab now returns a 200 OK status with all expected Access-Control-* headers. - Verify the subsequent actual data request (e.g.,
POST) also succeeds.