Skip to content

What is CORS? A practical guide for frontend developers

Why browsers block cross-origin requests, how preflight works, how to read the CORS error in your console, and the three ways to fix it.

The ProxifyEdge team Updated 4 min read

You call an API from your front end, nothing comes back, and the console shows something like this:

Access to fetch at 'https://api.example.com/data' from origin
'https://app.example.org' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.

So what is CORS, and why is it blocking you? The request was not necessarily refused. In most cases it reached the server, the server answered, and the browser then refused to let your script read the answer. That is CORS at work, and once you see what the browser is checking, every variant of this error becomes straightforward to diagnose.

The same-origin policy comes first

Browsers isolate pages by origin: the combination of scheme, host and port. https://app.example.org and https://api.example.org are different origins, and so are http://localhost:3000 and http://localhost:5173. By default, script on one origin can send a request to another but cannot read the response.

The reason is your users’ cookies. If any page could read any response, a malicious site could quietly fetch your webmail or your bank’s API while you were signed in, and read the result. The same-origin policy is what stops that.

What CORS adds

Cross-Origin Resource Sharing is the server’s way of saying “this response may be read by that origin”. It is a set of response headers, and the browser enforces them. The important ones:

  • Access-Control-Allow-Origin: the origin allowed to read the response, or * for any origin.
  • Access-Control-Allow-Methods and Access-Control-Allow-Headers: what a preflighted request may use.
  • Access-Control-Allow-Credentials: whether cookies may be included. It cannot be combined with *.
  • Access-Control-Expose-Headers: which response headers script may read beyond a small safe list.
  • Access-Control-Max-Age: how long a preflight answer may be cached.

Because the browser enforces these, CORS never affects curl, a server, or a mobile app. That is why the same request “works in Postman” and fails in the browser.

Simple requests and preflight

A simple request is a GET, HEAD or POST with only basic headers and a form-like content type. The browser sends it straight away and checks the CORS headers on the response.

Anything else is preflighted: a PUT or DELETE, a JSON body, or a custom header such as Authorization or X-API-Key. Before the real request, the browser sends an OPTIONS request asking for permission, and only proceeds if the answer allows it:

OPTIONS /data HTTP/1.1
Host: api.example.com
Origin: https://app.example.org
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, x-api-key

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.org
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: content-type, x-api-key
Access-Control-Max-Age: 600

If the preflight fails, the real request is never sent. If it succeeds, the real response still needs its own Access-Control-Allow-Origin. The guide to preflight requests goes deeper into exactly which requests trigger one.

Reading the error message

The console saysWhat it means
No ‘Access-Control-Allow-Origin’ header is presentThe server does not support CORS for your origin at all, or an error response skipped its CORS headers.
The ‘Access-Control-Allow-Origin’ header has a value that is not equal to the supplied originThe server allows a different origin. Check scheme, port and trailing slashes.
Response to preflight request doesn’t pass access control checkThe OPTIONS request failed or lacked the headers. Often a 401 or 404 on OPTIONS.
Request header field x-api-key is not allowed by Access-Control-Allow-HeadersThe preflight answer did not list a header your request uses.
The value of the ‘Access-Control-Allow-Origin’ header must not be the wildcard ’*’ when the request’s credentials mode is ‘include’You sent cookies (credentials: 'include') to a server that answers *.

For a walk through each of these in the browser’s network panel, see debugging CORS errors with DevTools.

Three ways to fix a CORS error

1. Configure the API, if you own it

The cleanest fix is for the API to send the right headers. Allow the specific origins that need access rather than *, answer OPTIONS without requiring authentication, and add Vary: Origin so caches keep the answers for different origins apart.

// Express: allow one known origin, not "*".
app.use((req, res, next) => {
  if (req.headers.origin === 'https://app.example.org') {
    res.set('Access-Control-Allow-Origin', 'https://app.example.org');
    res.set('Vary', 'Origin');
  }
  if (req.method === 'OPTIONS') {
    res.set('Access-Control-Allow-Methods', 'GET, POST');
    res.set('Access-Control-Allow-Headers', 'Content-Type, X-API-Key');
    return res.sendStatus(204);
  }
  next();
});

2. Call it from your own server

CORS only applies in the browser, so a route on your own backend can call the third-party API and return the result to your page. This also keeps any API secret on the server. The cost is that you now run, secure and scale that route.

3. Use a CORS proxy

When you cannot change the API and do not want to run a server, a CORS proxy makes the request on your page’s behalf and returns the response with the CORS headers added. With ProxifyEdge it is one URL prefix:

const target = 'https://api.example.com/data';

const response = await fetch(`https://api.proxifyedge.com/proxy?url=${encodeURIComponent(target)}`, {
  headers: { 'X-API-Key': 'pk_your_public_key' },
});
const data = await response.json();

A public proxy key is safe to ship in a browser bundle because a live key only works from the origins you list in the dashboard. If the upstream API needs its own credential, do not put it in the bundle: store it in ProxifyEdge’s secrets vault and reference it as {{secret.NAME}}, and ProxifyEdge substitutes the real value on the way out. The reasons are covered in why you can’t hide an API key in frontend code.

What not to do

  • mode: 'no-cors' does not bypass CORS. It produces an opaque response your script cannot read.
  • Browser extensions that disable CORS only work on your machine, never for your users.
  • Access-Control-Allow-Origin: * on an authenticated API lets any site read anything that does not need cookies. Allow named origins.
  • Open proxies with your API key in the URL expose that key to anyone who opens the network tab.

Key takeaways

  • A CORS error is the browser protecting your users; the server usually did answer.
  • The fix always lives on the server side of the request: the API’s headers, your own backend, or a proxy.
  • Read the exact console message and check whether a preflight is involved before changing anything.
  • Never fix CORS by weakening it for authenticated data, and never ship upstream secrets to the browser.

You can see the headers for yourself in the playground.

Are you sure?