Skip to content
Cloudzyro
Troubleshooting

CORS error: what it means and how to fix it

Kofi Agyem

, Founder and CEO, CloudzyroPublished · 7 min read

On this page
  1. 1. The short answer
  2. 2. What CORS is
  3. 3. Simple requests and preflight
  4. 4. The headers that matter
  5. 5. Common causes
  6. 6. How to fix it, step by step
  7. 7. Express
  8. 8. Next.js route handlers
  9. 9. Supabase
  10. 10. On Cloudzyro
  11. 11. How to check
  12. 12. Sources

A CORS error means the browser blocked a response because the server didn't allow your origin. How CORS and preflight work, with Express and Next.js fixes.

1. The short answer

A CORS error means your page asked a server on another origin for something, and the server's response didn't say your origin is allowed to read it, so the browser blocked it. The fix always lives on the server you're calling: it has to send Access-Control-Allow-Origin and, for some requests, a few more headers. Nothing you change in the browser or in your frontend code can grant that permission.

If you don't control that server, the reliable fix is to call it from your own backend, where CORS doesn't apply.

2. What CORS is

Browsers follow the same-origin policy: a script on one origin can't read responses from another. An origin is the scheme, host and port together. These are all different origins:

  • http://localhost:3000 and http://localhost:5173 (different port);
  • http://example.com and https://example.com (different scheme);
  • https://example.com and https://www.example.com (different host, see www vs non-www);
  • https://www.example.com and https://api.example.com.

Cross-Origin Resource Sharing (CORS) is how a server relaxes that rule. It adds response headers that tell the browser which other origins may read its responses. The browser does the checking. That's why the same request works in curl, Postman or server code: they don't enforce CORS at all.

The error appears in the browser's console, with wording like:

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

Your JavaScript only sees a vague TypeError: Failed to fetch. Browsers hide the details from scripts on purpose, so always read the console and the Network tab.

3. Simple requests and preflight

Some requests go straight to the server. A simple request is a GET, HEAD or POST with only basic headers and a Content-Type of text/plain, multipart/form-data or application/x-www-form-urlencoded. The browser sends it, then checks the response for Access-Control-Allow-Origin.

Everything else gets a preflight first: an OPTIONS request asking permission. That includes most API calls, because sending JSON (Content-Type: application/json), using PUT, PATCH or DELETE, or adding an Authorization header all trigger one.

OPTIONS /items HTTP/1.1
Origin: https://www.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Max-Age: 600
Vary: Origin

If the preflight answer is missing a header, lists the wrong ones, or comes back with a status outside 200 to 299 (a 404, a 401 from your login check, or a redirect), the browser never sends the real request.

4. The headers that matter

  • `Access-Control-Allow-Origin`: the origin allowed to read the response, such as https://www.example.com, or * for anyone. Only one value is allowed, so to accept several origins, check the request's Origin against your list and echo back the one that matches.
  • `Access-Control-Allow-Methods`: the methods allowed after a preflight.
  • `Access-Control-Allow-Headers`: the request headers allowed, such as content-type and authorization.
  • `Access-Control-Allow-Credentials: true`: needed when the request sends cookies (credentials: "include" in fetch). With it, Access-Control-Allow-Origin must be the exact origin, never *.
  • `Access-Control-Expose-Headers`: response headers your script may read beyond the basic ones.
  • `Access-Control-Max-Age`: how long, in seconds, the browser may reuse a preflight answer.
  • `Vary: Origin`: tells caches the response depends on the origin. Add it whenever you echo the origin back, or a CDN may serve one origin's answer to another.

5. Common causes

  • The server sends no CORS headers at all. The usual case with a new API.
  • The origin doesn't match exactly. The allow list has https://example.com but the page runs on https://www.example.com, or a trailing slash snuck in (https://example.com/ isn't an origin).
  • Wildcard with credentials. * plus cookies or credentials: "include" is always rejected.
  • The preflight fails. No OPTIONS handler, an auth check that rejects the OPTIONS request, or a missing header in Access-Control-Allow-Headers.
  • A redirect. The API redirects http to https, or adds a trailing slash, and the preflight can't follow it. Call the final address directly.
  • An error response without headers. The app crashes, the error handler answers 500 without CORS headers, and the browser reports CORS instead of the crash.
  • Two layers add the header. Both your app and a proxy add Access-Control-Allow-Origin, and the browser rejects the duplicate value.

6. How to fix it, step by step

  1. Open the Network tab and find the failing request. If there's an OPTIONS request before it, look at that first: its status and response headers.
  2. Check the status code. A 404, 401 or 500 means fix that first. CORS headers on an error page won't help.
  3. Compare origins character by character. Copy the page's origin from the address bar (scheme, host, port, no path) and compare it with the allowed value.
  4. Add the headers on the server, including on OPTIONS responses and error responses. Examples below.
  5. **Don't use * with cookies.** Name the origin and send Access-Control-Allow-Credentials: true.
  6. If it's not your server, move the call to your backend, or check whether the service expects you to call it from a server with a secret key anyway.

One thing never to do: turn off web security in your browser, or install an extension that strips CORS. It hides the problem on your machine only, and every visitor still sees the error.

7. Express

The cors package handles preflights and the headers for you:

// npm install cors
import express from "express";
import cors from "cors";

const app = express();

const allowed = (process.env.ALLOWED_ORIGINS || "").split(",");

app.use(
  cors({
    origin: allowed, // e.g. https://www.example.com,http://localhost:5173
    credentials: true, // only if you send cookies
  }),
);

app.use(express.json());
app.post("/items", (req, res) => res.json({ ok: true }));

Put app.use(cors(...)) before your routes and any auth middleware, so preflights are answered before a login check can reject them. Keeping the list in an environment variable means you can add your live domain without changing code. More on hosting Node.js apps.

8. Next.js route handlers

In the App Router, export an OPTIONS handler for the preflight and add the same headers to every response:

// app/api/items/route.ts
const allowed = ["https://www.example.com", "http://localhost:5173"];

function cors(origin: string | null) {
  const headers: Record<string, string> = {
    "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
    "Access-Control-Allow-Headers": "content-type, authorization",
    Vary: "Origin",
  };
  if (origin && allowed.includes(origin)) {
    headers["Access-Control-Allow-Origin"] = origin;
  }
  return headers;
}

export async function OPTIONS(req: Request) {
  return new Response(null, {
    status: 204,
    headers: cors(req.headers.get("origin")),
  });
}

export async function POST(req: Request) {
  const data = await req.json();
  return Response.json(
    { ok: true, data },
    { headers: cors(req.headers.get("origin")) },
  );
}

If your frontend and API are in the same Next.js app on the same domain, you don't need CORS at all: same-origin requests are never blocked. See hosting Next.js.

9. Supabase

Supabase's own APIs (the database API, Auth and Storage) already answer browsers from any origin, so a CORS error there usually isn't really about CORS: check the project URL and key in your settings, and look for a 401 or 404 in the Network tab.

Edge Functions are different. You write the responses, so you add the headers and answer the preflight yourself, as Supabase's docs show:

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers":
    "authorization, x-client-info, apikey, content-type",
};

Deno.serve(async (req) => {
  if (req.method === "OPTIONS") {
    return new Response("ok", { headers: corsHeaders });
  }
  const body = JSON.stringify({ ok: true });
  return new Response(body, {
    headers: { ...corsHeaders, "Content-Type": "application/json" },
  });
});

Include the headers on error responses too. Setting up a Supabase project for a site? See how to connect Supabase.

10. On Cloudzyro

On Cloudzyro Sites & Apps, a site that serves both its pages and its API from one address has no CORS to configure. If your frontend and API are separate sites, or you add a custom domain, add each address your pages load from to the API's allow list, for example with an environment variable set on the site's Environment variables tab, then deploy. Remember that www.example.com, example.com and yoursite.cloudzyro.app are three different origins.

11. How to check

Send a preflight yourself with curl and read the headers that come back:

curl -i -X OPTIONS https://api.example.com/items \
  -H 'Origin: https://www.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type'

You want a 2xx status and an Access-Control-Allow-Origin that exactly matches the origin you sent. Then reload the page with the Network tab open and confirm the real request goes out after the preflight.

12. Sources

Questions

What does a CORS error mean?
Your page asked a server on a different origin for data, and the server's response didn't include the headers that say your origin may read it. The browser received the response but refused to hand it to your code.
Can I fix a CORS error in the browser or in my frontend code?
No. The permission has to come from the server you're calling, in its response headers. Settings like mode no-cors don't fix it: they give your code an empty, unreadable response. If you don't control the server, call it from your own backend instead.
Why does the request work in curl or Postman but not in the browser?
CORS is enforced only by browsers. curl and Postman don't check the Access-Control headers, so they show the response even when a browser would block it.
What is a preflight request?
An OPTIONS request the browser sends first, before requests that aren't simple, such as a POST with a JSON body or any request with an Authorization header. If the preflight doesn't come back with the right headers and a 2xx status, the real request is never sent.
Is Access-Control-Allow-Origin: * safe?
For public data that needs no login, yes. It doesn't work with cookies or other credentials, though: browsers reject the wildcard on credentialed requests, so you must name the exact origin instead.
Why do I get a CORS error when the server is actually crashing?
Error responses often skip the middleware that adds CORS headers. The browser then reports a CORS problem, and the real 500 or 404 is hidden. Check the server's logs and the Network tab for the status code.
Are localhost:3000 and localhost:5173 the same origin?
No. An origin is the scheme, host and port together, so a different port is a different origin. So are http and https, and www and the bare domain.

Put your site online free

From your AI tool, Git or a zip, with every version kept.