CORS error when your app calls a Supabase Edge Function or API
"Blocked by CORS policy" in the console when your Lovable, Bolt, or Cursor app calls an Edge Function or your own API. What it really means and how to fix it.
What you are seeing
- The console shows "Access to fetch at '...' from origin '...' has been blocked by CORS policy: Response to preflight request doesn't pass access control check".
- Or: "No 'Access-Control-Allow-Origin' header is present on the requested resource."
supabase.functions.invokefails with "Failed to send a request to the Edge Function".- The same request works from curl or Postman but fails in the browser.
- It works on localhost and fails on the deployed domain, or the reverse.
Why it happens
- The function never answers the
OPTIONSpreflight. Browsers send it before any request with a JSON body or anAuthorizationheader. - CORS headers are added to the success response but not to errors. When the function throws or returns 401, the browser reports a CORS error and hides the real one.
- The allowed headers list leaves out
authorization,apikey,x-client-info, orcontent-type, which supabase-js sends. Access-Control-Allow-Origin: *is combined with cookies orcredentials: 'include', which browsers reject.- Only localhost or the preview domain is allowed, not the production domain.
- The browser calls a third-party API directly that does not allow browser origins. That call belongs on a server or in an Edge Function, which also keeps the key secret.
How to fix it
1. Read the real request in DevTools
Open the Network tab, reproduce the error, and look at both the OPTIONS request and the request after it. A 404, 401, or 500 there is the actual problem; CORS is just how the browser reports it.
2. Define one set of CORS headers
// supabase/functions/_shared/cors.ts
export const corsHeaders = {
'Access-Control-Allow-Origin': 'https://yourapp.com',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
};
3. Answer the preflight and add headers to every response, errors included
import { corsHeaders } from '../_shared/cors.ts';
Deno.serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
const body = await req.json();
// ... your logic
return new Response(JSON.stringify({ ok: true }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
} catch (err) {
return new Response(JSON.stringify({ error: String(err) }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
});
4. Redeploy the function
supabase functions deploy my-function
5. Test the preflight from your terminal
curl -i -X OPTIONS https://<project-ref>.supabase.co/functions/v1/my-function \
-H "Origin: https://yourapp.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: authorization, content-type"
6. Same fix for an Express API
import cors from 'cors';
app.use(cors({
origin: ['https://yourapp.com', 'http://localhost:5173'],
credentials: true,
}));
How to know it is fixed
On the deployed site, the Network tab shows the OPTIONS request returning 200 or 204 with an Access-Control-Allow-Origin that matches your domain, and the real request returning your JSON. Trigger a deliberate error and confirm the app now shows that error instead of a CORS message. CORS only controls what browsers may read, so keep your auth checks on the server either way.