How to Fix CORS Errors in Local Development
CORS is not a bug in your code and not a problem with the browser. It is the browser refusing to hand your JavaScript a cross-origin response that the server did not authorize. The request usually succeeded. What failed was reading it. That distinction explains why the fix is always a response header and never a change to your fetch call.
Read the error before you change anything
| Message contains | Meaning | Fix |
|---|---|---|
No Access-Control-Allow-Origin header is present | the server did not authorize your origin at all | set that header to your exact origin |
| does not match the supplied origin | the server allowed a different origin, often a hardcoded production URL | set it to your dev origin |
| Response to preflight request does not pass | the OPTIONS request failed, so the real request never happened | answer the preflight with the allow headers below |
credentials mode is include | wildcard and credentials cannot be combined | echo the exact origin and add Access-Control-Allow-Credentials: true |
| Request header field ... is not allowed | you sent a header the server did not list | add it to Access-Control-Allow-Headers |
The header rule that unblocks a local frontend
Match: domain is api.example.com
Action: set response header Access-Control-Allow-Origin: http://localhost:3000
set response header Access-Control-Allow-Credentials: true
set response header Access-Control-Allow-Headers: authorization, content-type
set response header Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Use your exact origin, including the scheme and port. Access-Control-Allow-Origin: * looks convenient and then fails the moment cookies are involved, because the specification forbids the wildcard on credentialed requests. Echoing one specific origin is both stricter and less likely to waste your afternoon.
Preflight
Anything beyond a simple GET or POST with ordinary headers triggers an OPTIONS preflight first. The browser sends it without your Authorization header, listing what it intends to send in Access-Control-Request-Headers. The response to that preflight has to authorize the method and the headers, which is why Access-Control-Allow-Headers and Access-Control-Allow-Methods belong in the rule even though the failing request is a GET. Adding Access-Control-Max-Age: 600 cuts the repeat preflights while you work.
If the server returns 404 or 405 for OPTIONS, headers alone will not save you. The browser requires a successful preflight response, and no extension can invent one where the server refuses to answer. That case needs a proxy or a server-side change.
Reading custom response headers
A cross-origin response only exposes a short list of headers to JavaScript. If your code needs to read X-Request-Id or a pagination header, the server has to list it in Access-Control-Expose-Headers, and until it does, response.headers.get returns null even though DevTools shows the header plainly. This one confuses everybody once.
Choose the right fix for the situation
| Approach | Good for | Cost |
|---|---|---|
| Header rule in the browser | unblocking yourself now, confirming that CORS is genuinely the problem | local to you, has to be turned off later, fixes nothing for anyone else |
| Dev server proxy (Vite, Next.js, webpack) | the everyday answer for a frontend calling an API | a few lines of config. Same-origin from the browser’s point of view, so CORS never applies |
| Server-side CORS configuration | the real fix, and the only one that works in production | needs access to the API and a decision about which origins to trust |
| Disabling web security in a browser flag | nothing. Do not | turns off protections for every site in that profile |
// vite.config.js: same-origin from the browser's point of view
export default {
server: {
proxy: { '/api': { target: 'https://api.example.com', changeOrigin: true } },
},
};
A useful sequence: unblock with a header rule to confirm the diagnosis in thirty seconds, switch to a dev-server proxy for daily work, and open a ticket for the CORS configuration that actually ships. The header rule is a diagnostic, not a solution.
Things that look like CORS and are not
- A failed request with no response at all: DNS, TLS, or the server being down. CORS errors involve a response you are not allowed to read.
- A 401 or 403 from the API. Auth failures are not CORS failures, though a missing CORS header can hide the real status from your code.
- Mixed content: an HTTPS page calling
http://is blocked by a different policy, and no CORS header helps. - A Content Security Policy
connect-srcrule blocking the call from your own page. The error text names CSP, not CORS.
Common questions
Does adding CORS headers in my browser fix CORS for my users?
No. The header is applied inside your browser after the response arrives, so only you see it. Every other visitor gets whatever the server sends. Use it to confirm the diagnosis, then fix the server or add a dev-server proxy.
Why does the request work in curl or Postman but fail in the browser?
Because CORS is enforced by browsers, not servers. curl and Postman have no origin to protect, so they never apply the check. A request that works in curl and fails in the browser is almost always a CORS problem rather than a broken endpoint.
Can I just use Access-Control-Allow-Origin: *?
For requests without credentials, yes. As soon as the request sends cookies or uses credentials mode include, the specification forbids the wildcard and the browser rejects the response. Echoing your exact origin works in both cases, so it is the better habit.
Why is my preflight failing when my GET request should be simple?
Something made it non-simple. A custom header such as Authorization, a content type other than the three simple ones, or a method beyond GET, HEAD, and POST all trigger a preflight. Look at the OPTIONS entry in the Network panel and see which header the server refused.