Skip to content
← Back to the blog
software developmentintegrations

CORS error explained: why it happens and how to fix it

What a CORS error really means, how to read it in the browser console, the usual causes in AI-built apps, and the server-side fix for Supabase functions.

By Devonauts Team6 min read
Leer en español →
A laptop with a red backlit keyboard shows HTML code in an editor in a dark room

A CORS error means the browser refused to let your front end read a response from another origin, because that server didn't send headers saying your site is allowed. The fix almost always belongs on the server you're calling (or in a small backend function that calls it for you), not in the front-end code. That's also why the same request works in Postman or curl: CORS is enforced only by browsers.

This guide explains what an origin is, how to read the exact message in the console, the causes that come up most in apps built with Lovable, Bolt, v0 or Cursor, and how to fix and verify each one.

What counts as a different origin?

An origin is the combination of scheme, host and port. If any of the three differs, the browser treats it as a different site.

Your pageRequest goes toSame origin?
https://yourapp.comhttps://yourapp.com/api/ordersYes
https://yourapp.comhttps://www.yourapp.com/apiNo, different host
http://localhost:5173http://localhost:3000No, different port
https://yourapp.comhttps://abc.supabase.co/functions/v1/sendNo, different host

Calls from your app to a Supabase Edge Function, a third-party API or your own backend on another domain are all cross-origin, so they all need CORS headers.

How do I read the error message?

Open the browser's developer tools, Console tab. In Chrome the message usually looks like this:

Access to fetch at 'https://abc.supabase.co/functions/v1/send' from origin
'https://yourapp.com' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.

Firefox phrases it as "Cross-Origin Request Blocked" followed by a reason, such as CORS header 'Access-Control-Allow-Origin' missing. MDN keeps a list of these reasons with an explanation for each.

Then switch to the Network tab and find the failed request. Two details tell you most of the story:

  • Is there an OPTIONS request before it? That's the preflight. If it failed, the problem is the preflight response.
  • What status code did the real request get? A 500 or 404 with a CORS error usually means the server crashed or the URL is wrong, and the error page simply didn't include CORS headers.

What is a preflight request?

For many requests the browser first sends an OPTIONS request asking the server whether the real one is allowed. It does this when the method isn't GET, HEAD or POST, when you send headers like Authorization, or when Content-Type is anything other than form data or plain text. A JSON body with Content-Type: application/json triggers a preflight, which means most API calls from a modern app do.

The server must answer that OPTIONS request with a success status and headers that allow your origin, method and headers. If the server's code expects a JSON body and crashes on the empty preflight, you get a CORS error even though the real request would have worked.

What usually causes CORS errors in AI-built apps?

  1. An Edge Function that doesn't handle OPTIONS. Very common with generated Supabase functions. The function runs its main logic on the preflight, fails, and returns no CORS headers.
  2. Headers only on the success path. The function adds CORS headers when everything works, but an error (missing variable, bad input) returns a plain response without them. The console shows a CORS error and hides the real problem.
  3. Calling a third-party API straight from the browser. Many APIs deliberately don't allow browser calls, and the call usually needs a secret key that must not be in front-end code anyway.
  4. The allowed-origin list is out of date. The server allows https://yourapp.com but you're now on www.yourapp.com, a new custom domain or a preview URL.
  5. Credentials with a wildcard. If you send cookies (credentials: 'include'), the server may not answer with Access-Control-Allow-Origin: *; it must name your exact origin and also send Access-Control-Allow-Credentials: true.
  6. A redirect on the way. The URL redirects from http to https or adds a trailing slash, and the redirect response lacks CORS headers.

How do I fix it?

For a Supabase Edge Function

Handle the preflight first and attach the same headers to every response, including errors. Supabase's CORS guide recommends importing the headers from the client library (version 2.95.0 or later):

import { corsHeaders } from 'npm:@supabase/supabase-js@^2/cors'

export default {
  fetch: async (req: Request) => {
    if (req.method === 'OPTIONS') {
      return Response.json({ ok: true }, { headers: corsHeaders })
    }
    try {
      const body = await req.json()
      // ... your logic
      return Response.json({ ok: true }, { headers: corsHeaders })
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error)
      return Response.json({ error: message }, { status: 400, headers: corsHeaders })
    }
  },
}

Those default headers allow any origin. For functions that act on behalf of a logged-in user, that's normally fine because the function still checks the user's token. If you want to restrict it, replace the allowed origin with your own domain.

For your own API

Allow only the origins you actually use, answer OPTIONS, and list the methods and headers your front end sends. A typical response looks like:

Access-Control-Allow-Origin: https://yourapp.com
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: authorization, content-type
Vary: Origin

For a third-party API

Don't call it from the browser. Put the call in a server function (an Edge Function, a Vercel or Netlify function) that holds the key and returns only what the front end needs. Our guide to an API key exposed in the front end explains why that matters. Alternatively, a rewrite on your host can make the browser see the API as same-origin. On Vercel that's a rewrite in vercel.json:

{ "rewrites": [{ "source": "/api/:path*", "destination": "https://api.example.com/:path*" }] }

Only use a rewrite for APIs that don't need a secret, since the browser still sends the request.

What should I not do?

  • Don't use mode: 'no-cors'. The request goes out, but the response becomes unreadable, so your code gets nothing useful.
  • Don't install a browser extension that disables CORS. It hides the problem on your machine only.
  • Don't allow every origin with credentials. Browsers refuse that combination anyway.

How do I confirm it's fixed?

In the Network tab, the OPTIONS request should return 200 or 204 with Access-Control-Allow-Origin matching your page's origin, and the real request should show the same header. Test on your production domain and a preview URL, logged in and logged out. Then trigger an error on purpose (bad input) and check you see the real error message, not a CORS error.

Quick answers

Why does it work in Postman but not in the browser? CORS is a browser rule. Postman and curl don't enforce it, so they only prove the server responds, not that browsers may read it.

Can I fix CORS in my React code? No. The browser checks the server's response headers, so the server has to change.

Why did CORS break after I connected my domain? The server or function allows a specific origin, and your new domain or www isn't on the list.

Is a CORS error a security problem? It's the browser protecting your users. The risk is the shortcut: allowing every origin with credentials, or moving a secret key into the browser to dodge the error.

Still seeing CORS errors?

If the console keeps showing CORS and you can't tell what the server is really saying, our API integration and maintenance and support work covers exactly this. See also our guide to Lovable Edge Function errors, or run the free health check.

Still stuck with your app?

Run the free health check on your app's URL, or get the $299 App Rescue Audit: a senior engineer reviews the code and sends you a written report in 2 business days.