Why a CORS Error Is a Server Fix, Not a Browser Bug
The red line in the console reads something like "has been blocked by CORS policy", and the front-end developer staring at it starts changing fetch options. They add headers to the request, switch the mode, try a different library.

The red line in the console reads something like "has been blocked by CORS policy", and the front-end developer staring at it starts changing fetch options. They add headers to the request, switch the mode, try a different library. Nothing works, because nothing on their side can work. The browser is enforcing a rule, and the only party who can satisfy that rule is the server answering the request.
I spent years in cross-browser QA, and CORS tickets were a reliable source of wasted afternoons, usually bounced between front-end and back-end teams who each believed the other one had the bug. This piece is about reading the error correctly, finding which server response is missing what, and avoiding the workarounds that make it worse.
What the browser is actually protecting
Browsers apply the same-origin policy: a script loaded from one origin, meaning the combination of scheme, host and port, cannot freely read responses from another origin. Without it, any page you visit could quietly call your bank's API using your logged-in cookies and read the answer.
Cross-Origin Resource Sharing is the agreed way for a server to relax that rule for specific callers. The server says, through response headers, which origins may read its responses, which methods and headers they may use, and whether cookies may come along. The browser reads those headers and decides. That is why the fix lives on the server: the browser is waiting for permission, and only the server can grant it.
One detail surprises people. For a plain GET or a basic form-style POST, the browser sends the request first and checks the permission headers on the response afterward. So the request usually reached your server and ran. Only the reading of the result was blocked. If that request wrote to a database, the write happened even though the console shows an error.
The preflight is where most failures hide
For anything beyond those basic requests, the browser asks first. A request with a JSON content type, an Authorization header, or a method like PUT, PATCH or DELETE triggers a preflight: an OPTIONS request to the same URL, asking whether the real request would be allowed. If the preflight answer is wrong in any way, the real request is never sent.
This is where I would open DevTools before touching any code. In the Network panel, clear the log, reproduce the call and look for two entries for the same URL: the OPTIONS preflight and, if it got that far, the real request. Some browsers hide preflights behind a filter or show them grouped, so it is worth checking the panel options; the DevTools features most developers never open covers a few of those settings. Click the OPTIONS entry and read its status and response headers. The console message tells you which header the browser wanted; the Network panel tells you what the server actually sent.
Reading the symptom back to the server
Console wording differs between Chrome, Firefox and Safari, but the causes repeat. These are the patterns I see most, as typical cases rather than an exhaustive list.
| What you see | Likely cause on the server |
|---|---|
| No Access-Control-Allow-Origin header on the response | CORS not configured for this route, or only configured on success responses |
| Preflight returns 401 or 403 | Authentication middleware runs before the CORS handler and rejects the OPTIONS request, which never carries credentials |
| Preflight returns 404 or 405 | The framework has no handler for OPTIONS on that path |
| Header not allowed by Access-Control-Allow-Headers | The client sends a custom header, such as Authorization, that the server did not list |
| Wildcard origin not allowed with credentials | The server answers with an asterisk while the request includes cookies |
| Redirect not allowed for preflight | The URL redirects, often a missing trailing slash or http to https |
The first row hides a nasty variant. When the server throws a 500, many frameworks skip the middleware that adds CORS headers. The browser then reports a CORS error instead of the real crash, and the team chases the wrong problem. If an endpoint "sometimes has CORS errors", check the server logs for exceptions at those times before anything else.
Credentials, wildcards and the Vary header
Cookies change the rules. If the front end sends credentials, the server must name the exact origin in Access-Control-Allow-Origin, not an asterisk, and must also send Access-Control-Allow-Credentials set to true. Many servers handle multiple allowed origins by checking the incoming Origin header against a list and echoing it back. That is fine, as long as the response also includes Vary: Origin. Without it, a cache or CDN may store the response with one origin's permission and serve it to another, which produces errors that come and go depending on who loaded the page first.
Whether the cookies are even sent is a separate question, decided by their SameSite and Secure attributes. How browsers decide which cookies to send walks through that side, and it is worth checking both when a logged-in call fails across subdomains.
The browser is not broken. It is waiting for a permission slip the server forgot to write.
Workarounds that make it worse
The most common advice on forums is to set the fetch mode to no-cors. It makes the error disappear and leaves you with an opaque response: the request goes out, but your script cannot read the status or the body. For an API call, that is useless, and it hides the real failure.
The second is a browser extension or a launch flag that disables web security. It works on your machine and nowhere else, and it trains a team to test in a browser no user will ever run. The third is reflecting any Origin back with credentials allowed, which effectively switches the protection off for every site on the internet. If the API serves logged-in data, that is a security hole, not a fix.
A development proxy is the legitimate shortcut. Dev servers can forward API calls through the same origin as the page, so no cross-origin request happens locally. It is fine for development, as long as everyone remembers production still needs proper headers.
If you are designing the API from scratch, deciding early which origins and headers it supports saves this whole fight, and it belongs alongside the other conventions in designing a REST API other developers enjoy.
Try this on your next CORS ticket
Before changing any code, reproduce the failing call with the Network panel open, find the OPTIONS request, and copy its status code and response headers into the ticket. Then check the server logs for an exception at the same timestamp. Those two pieces of evidence usually tell the back-end team exactly which handler to fix, and end the back and forth.
More in Back-End
Back-End
How Rate Limiting Protects an API Without Punishing Real Users
Every public API eventually meets a client that sends far more requests than any reasonable use case requires, whether that is...
Back-End
How Online Game Platforms Scale Their Servers on Busy Nights
Friday evening, 8 p.m. local time. A new season of an online game goes live, a popular streamer starts playing, and within ten...
Back-End
How to Design a REST API Other Developers Enjoy Using
The best compliment an API can get is silence. Nobody files a ticket, nobody asks in the shared channel what a field means, and...