What this usually means
The browser enforces CORS (Cross-Origin Resource Sharing) when a web page from one origin tries to access resources from a different origin. In local dev, you might run everything on `localhost` and the origins match. In production, your frontend is on `app.example.com` and your API is on `api.example.com` — different origins. The server must explicitly allow the frontend origin by sending the right CORS headers. If the deployed server's CORS config does not include the deployed frontend's origin, the browser blocks the response.
The first ten minutes \u2014 establish facts before touching code.
- 1Open browser DevTools → Network tab. Find the failing request. Check the Response Headers for `Access-Control-Allow-Origin`. If it is missing or wrong, that is the problem.
- 2Check the Request URL origin (e.g. `https://app.example.com`) against the server's CORS allowed origins list. Does the server allow that exact origin?
- 3Look for an OPTIONS (preflight) request before the main request. If OPTIONS returns a non-2xx status, the preflight is failing.
- 4Verify the server is sending `Access-Control-Allow-Origin` as a single value, not a comma-separated list (browsers reject multiple values).
- 5Check if the server's CORS middleware runs before your route handlers. If a route returns a response before CORS headers are added, the browser blocks it.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchServer CORS configuration (Express `cors()` middleware options, Fastify `@fastify/cors`, Django `django-cors-headers`)
- searchReverse proxy or API gateway CORS settings (Nginx, Cloudflare, AWS API Gateway)
- searchDeployment platform's custom domain and SSL settings — origin mismatch after redirect
- searchFrontend code — the actual `fetch` or `axios` request URL (is it the production API URL or still localhost?)
- searchBrowser DevTools Network tab — request and response headers for the failing call
- searchServer logs — look for OPTIONS requests and their response status codes
Practical causes, not theory. These are the things you will actually find.
- warningCORS config only allows `http://localhost:3000` — production origin is not added
- warningServer CORS middleware is applied after route handlers, so error responses skip it
- warningCredentials mode (`withCredentials: true`) requires `Access-Control-Allow-Origin` to be a specific origin, not `*`
- warningPreflight OPTIONS request hits a different server or route that does not handle CORS
- warningA redirect (e.g. HTTP to HTTPS, or apex to www) changes the origin mid-request
- warningA reverse proxy or CDN strips CORS headers from responses
- warningThe deployed API URL has a typo or missing path segment, hitting a wrong endpoint
Concrete fix directions. Pick the one that matches your root cause.
- buildAdd the production frontend origin to the server's CORS allowed origins list — exact match, with protocol
- buildPlace CORS middleware before all route handlers so error responses also get CORS headers
- buildConfigure the server to respond to OPTIONS preflight requests with 204 and the correct CORS headers
- buildIf using credentials, set `Access-Control-Allow-Origin` to the specific origin (not `*`) and add `Access-Control-Allow-Credentials: true`
- buildIn development, use a proxy (Vite `server.proxy`, Next.js `rewrites`) so the browser sees same-origin requests
A fix you cannot prove is a guess. Close the loop.
- verifiedOpen the deployed frontend in a browser, make the API call, and check the Network tab for a 200 response with correct CORS headers.
- verifiedUse curl to simulate the preflight: `curl -X OPTIONS -H 'Origin: https://your-frontend.com' -H 'Access-Control-Request-Method: POST' https://api.example.com/endpoint -v`.
- verifiedTest with credentials: confirm both `Access-Control-Allow-Origin` (specific) and `Access-Control-Allow-Credentials: true` headers are present.
- verifiedDeploy to staging with production-like origins and confirm CORS works before production deploy.
- verifiedCheck that error responses (400, 500) also include CORS headers — test by triggering a known error.
Things that make this bug worse or harder to find.
- warningSetting `Access-Control-Allow-Origin: *` in production as a permanent fix — it breaks credentialed requests and is a security risk
- warningAdding the frontend origin to CORS config but forgetting the protocol (`https://` vs `http://`)
- warningDisabling CORS entirely with a browser extension or `--disable-web-security` flag
- warningAssuming CORS is a frontend problem — it is a server response header issue
- warningNot testing CORS on error responses — a 500 with missing CORS headers is just as broken as a 200