Skip to content
Fixing API errors

How to fix CORS errors when calling a public API from the browser

Your fetch works in curl but fails in the browser. Here is what the CORS error actually means, the four fixes that work, and the two pieces of advice that will waste your afternoon.

SandyPublished 6 min read
blue and white metal fence, illustrating how to fix cors errors when calling a public api from the browser
Photo by Hermes Rivera on Unsplash.

You wrote a fetch call. It works perfectly in curl. In the browser it fails, and the console says something like this:

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

The frustrating part is that the request usually succeeded. The server received it, processed it, and sent a response back. The browser then refused to let your JavaScript read it.

Short answer

You cannot fix this in your frontend code. CORS is enforced by the browser based on a header the server sends, and nothing you write on the client changes what the server sends. You have two real options: call an API that supports CORS, or move the request to a server you control. Everything else is a workaround or a trap.

What the error actually means

Browsers enforce the same-origin policy. A page served from one origin cannot read responses from a different origin unless that other origin explicitly allows it. An origin is the combination of scheme, host and port, so https://example.com and https://api.example.com are different origins, and so are http://localhost:3000 and http://localhost:8080.

The permission is granted by a response header:

Access-Control-Allow-Origin: *

If that header is present and matches your origin, the browser hands the response to your code. If it is missing, the browser blocks it and logs the error you just saw.

Two details explain most of the confusion:

The request was not blocked. For a simple GET, the browser sends it, the server handles it, and the response comes back. The block happens at the last moment, when the browser checks the headers before exposing the response. Any side effect on the server already happened.

Only browsers care. curl, Postman, your backend and your test runner do not implement the same-origin policy. That is why the endpoint works everywhere except the one place you need it.

How common is this, really?

We probe every API in our catalogue with a real Origin header and record what comes back, which lets us answer this with measurements instead of guesswork.

Fix 1: use an API that supports CORS

The fastest fix is often to pick a different API. If you are building something client-side and the first API you found does not support CORS, a competing one probably does.

These are all currently responding, need no API key, and send CORS headers. Latency is our own measurement.

APIKey neededCORSStatus
Open-MeteoGlobal weather forecast API for non-commercial useNoYesLive
DWD APIAPI of the German Weather Service (DWD): weather data, data from speciNoYesLive
NewtonSymbolic and Arithmetic Math CalculatorNoYesLive
Europe PMCLife-science literature search with abstracts, citations and full-textNoYesLive
BrazilCommunity driven API for Brazil Public DataNoYesLive

You can filter the whole catalogue this way: our browser-ready collection lists every API that needs no key, supports CORS and runs over HTTPS, which is exactly the set you can call from a static site with no backend at all.

Fix 2: proxy through your own server

When you need a specific API and it does not support CORS, put your own server in front of it. Your frontend calls your origin, which is same-origin and therefore unrestricted. Your server calls the third party, where CORS does not apply.

In Next.js that is a route handler:

app/api/weather/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const city = searchParams.get('city');
 
  if (!city) {
    return Response.json({ error: 'city is required' }, { status: 400 });
  }
 
  const upstream = await fetch(
    `https://api.example.com/weather?q=${encodeURIComponent(city)}`,
    {
      headers: { Accept: 'application/json' },
      // Cache upstream responses so you are not hammering a free tier.
      next: { revalidate: 600 },
    },
  );
 
  if (!upstream.ok) {
    return Response.json(
      { error: 'upstream failed' },
      { status: upstream.status },
    );
  }
 
  return Response.json(await upstream.json());
}

Your client code then calls a same-origin path, and the CORS error disappears because there is no longer a cross-origin request:

const response = await fetch('/api/weather?city=Lisbon');
const data = await response.json();

This approach has three advantages beyond fixing CORS. You can cache responses and stay inside a free tier. You can keep an API key server-side instead of shipping it to every visitor. And you can reshape an awkward third-party response into something your components actually want.

Fix 3: a serverless function, if you have no server

No backend is not a reason to give up on this. Netlify Functions, Cloudflare Workers and Vercel Functions all give you a single endpoint without running infrastructure, and the free tiers are generous enough for a portfolio project.

netlify/functions/quote.js
export async function handler() {
  const response = await fetch('https://api.example.com/quote');
  const data = await response.json();
 
  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 'public, max-age=300',
    },
    body: JSON.stringify(data),
  };
}

Fix 4: if it is your own API, send the header

When you control the server, this is a one-line change. In Express:

import cors from 'cors';
 
app.use(
  cors({
    origin: ['https://yourapp.com', 'http://localhost:3000'],
  }),
);

Name your origins rather than reaching for origin: '*'. A wildcard is fine for a genuinely public read-only API, but it cannot be combined with credentials, and it means any site on the web can call yours from their users' browsers.

If you are writing the headers yourself, a preflight needs more than the one header:

Access-Control-Allow-Origin: https://yourapp.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

That last one matters for performance. Without it the browser re-runs the preflight constantly, doubling your request count.

What does not work

These come up in every search result on this topic, and all three will cost you time.

mode: 'no-cors'

// This does not do what the name suggests.
const response = await fetch(url, { mode: 'no-cors' });

It does not disable CORS. It switches the request to a mode that returns an opaque response: status is always 0, headers are empty, and response.json() throws. The console error goes away, which is why people think it worked, and then they spend twenty minutes wondering why their data is undefined. Unless you are deliberately fire-and-forgetting a request whose response you do not need, this is not what you want.

Disabling browser security

Launching Chrome with --disable-web-security makes the error stop locally. It changes nothing for anyone visiting your site in a normal browser. Anything that only works with security disabled is not working, and you will discover this at deploy time.

Setting Access-Control-Allow-Origin on your request

// Meaningless. This is a response header.
fetch(url, {
  headers: { 'Access-Control-Allow-Origin': '*' },
});

Access control headers are sent by the server to describe its own policy. Sending one on a request is like writing your own name on someone else's guest list. Worse, adding a non-standard header can trigger a preflight that was not there before, turning a working request into a failing one.

Checking before you write any code

You can answer the question in one command. Send an Origin header and look at what comes back:

curl -s -I -H "Origin: https://example.com" https://api.open-meteo.com/v1/forecast \
  | grep -i access-control-allow-origin

If you see access-control-allow-origin: *, it will work from the browser. Silence means it will not.

For anything in our catalogue you can skip the command. Every listing shows a CORS badge based on exactly this check, re-run daily, so you can filter for browser-compatible APIs before writing a line of code.

Choosing between the fixes

SituationDo this
Any API will doPick one that supports CORS
You need this specific API and have a backendProxy it
You need this specific API and have no backendServerless function
The API needs a keyProxy it regardless, so the key stays server-side
It is your APISend the header, naming allowed origins

The thing worth remembering

CORS is not a bug, an obstacle, or something to be defeated. It is the reason a random web page cannot read your email from your browser session while you are logged in. The error is the browser doing its job.

Once you stop looking for a client-side fix, the problem becomes small. Either the API grants permission, or you make the call from somewhere the rule does not apply.

Common questions

Can I fix a CORS error with JavaScript in my frontend?

No. CORS is enforced by the browser based on headers the server sends. Nothing you write in your frontend can change what headers arrive, so no fetch option, header or library fixes it. The fix is always either to use an API that sends the right header, or to move the request off the browser.

Does mode: 'no-cors' fix a CORS error?

No, and it usually makes debugging harder. It stops the error appearing in the console but returns an opaque response, so response.json() fails and the status is always 0. You have hidden the error rather than solved it.

Why does the request work in curl or Postman but fail in the browser?

Because CORS is a browser rule, not a server rule. curl and Postman do not enforce it. A successful curl only proves the endpoint exists; it tells you nothing about whether a browser will let your page read the response.

Is disabling web security in Chrome a valid fix?

Only as a momentary local diagnostic, and not really even then. It changes nothing for your users, who run normal browsers. Anything that works only with security disabled is not working.

How do I know whether an API supports CORS before I write any code?

Send a request with an Origin header and look for Access-Control-Allow-Origin in the response. We do this automatically for every API in our catalogue and publish the result on each listing.

Sources

Written by

Sandy

I build and run this site on my own: the crawler that assembles the catalogue, the checker that probes every listing, and the writing. Before this I built SaveFromInternet and GrabReels, which meant living with other people’s APIs full time — parsers breaking when a platform shipped a change, rate limits arriving without warning, endpoints disappearing overnight. This directory exists because I got tired of free API lists that had never been checked.

APIs mentioned in this article

Open-Meteo

Environment

Global weather forecast API for non-commercial use

No keyCORSHTTPS

Verified 4 days ago: 100% uptime

View Details

DWD API

Weather

API of the German Weather Service (DWD): weather data, data from specific weather stations, warnings (local, coast, sea, alps)

No keyCORSHTTPS

Verified 4 days ago: 100% uptime

View Details

Newton

Science & Math

Symbolic and Arithmetic Math Calculator

No keyCORSHTTPS

Verified 4 days ago: 100% uptime

View Details

Europe PMC

Science & Math

Life-science literature search with abstracts, citations and full-text links

No keyCORSHTTPS

Verified 4 days ago: 100% uptime

View Details

Brazil

Government

Community driven API for Brazil Public Data

No keyCORSHTTPS

Verified 4 days ago: 100% uptime

View Details

Read next