HeHeedza

How do I fix the CORS error when calling a Supabase Edge Function?

Last reviewed

Short answer

Your function must answer the browser's OPTIONS preflight request and attach CORS headers to every response, including errors. Import corsHeaders from npm:@supabase/supabase-js@^2/cors (supabase-js v2.95.0+), return early on OPTIONS, and add the headers to success and error responses. Or wrap the handler with withSupabase, which handles CORS for you.

Why this happens

When a browser app calls supabase.functions.invoke(), it sends extra headers (authorization, apikey, x-client-info, content-type). That makes the browser send an OPTIONS preflight first. If the function doesn't reply to it with the right Access-Control-Allow-* headers, the browser blocks the real request and reports a CORS error.

The sneaky version: the function does handle CORS on success, but it crashes or returns a 400 or 500 without the headers. The browser then shows a CORS error instead of your real error message, and you go hunting in the wrong place.

Other causes that look like CORS:

  • The function isn't deployed, or invoke('name') doesn't match the function's name.
  • Your hardcoded header list is missing headers newer clients send, such as x-retry-count, or the trace headers when trace propagation is turned on.

How to fix it, step by step

1. Use the shared CORS headers (supabase-js v2.95.0 and later), following Supabase's docs:

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

export default {
  fetch: async (req: Request) => {
    // 1. Answer the preflight
    if (req.method === 'OPTIONS') {
      return Response.json({ ok: true }, { headers: corsHeaders });
    }
    try {
      const { name } = await req.json();
      // ... your logic
      return Response.json({ message: `Hello ${name}!` }, { headers: corsHeaders });
    } catch (error) {
      // 2. Errors need CORS headers too
      const message = error instanceof Error ? error.message : String(error);
      return Response.json({ error: message }, { status: 400, headers: corsHeaders });
    }
  },
};

2. On an older supabase-js, hardcode the headers in supabase/functions/_shared/cors.ts:

export const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers':
    'authorization, x-client-info, apikey, content-type, x-retry-count, traceparent, tracestate, baggage',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
};

3. Or let withSupabase do it. Supabase's @supabase/server wrapper handles CORS and preflight automatically:

import { withSupabase } from 'npm:@supabase/server@^1';
export default {
  fetch: withSupabase({ auth: 'user' }, async (req, ctx) => Response.json({ ok: true })),
};

4. Redeploy with supabase functions deploy <name>, then confirm the name in your code matches: supabase.functions.invoke('<name>').

5. Read the function logs. In the dashboard under Edge Functions → your function → Logs (or the Cloud view's logs on Lovable Cloud), check whether your code ran at all, and what the real error was if it did.

6. Lock it down later. Once it works, you can replace '*' with your real origin, e.g. https://yourdomain.com.

Fix prompt for your AI builder

Paste this into Lovable, Bolt, Cursor, Claude, ChatGPT, Replit, v0 or whichever tool you build with. Replace the bracketed parts first.

Fix prompt
Calling our Supabase Edge Function "<function-name>" from the browser fails with a CORS error ("No 'Access-Control-Allow-Origin' header"). Please fix only that function:
1. Import corsHeaders from 'npm:@supabase/supabase-js@^2/cors' (or a _shared/cors.ts that includes authorization, x-client-info, apikey, content-type, x-retry-count).
2. Return early on OPTIONS with corsHeaders.
3. Wrap the logic in try/catch and include corsHeaders on every response, including 4xx and 5xx errors.
4. Check that the name in supabase.functions.invoke matches the deployed function name.
Don't change the function's business logic or other functions.

How to catch this next time

CORS breaks come back when an AI edit rewrites a function and drops the shared headers, or when you upgrade supabase-js and the client starts sending a new header.

Heedza checks on a schedule that your live signup, login and checkout pages load and show their forms, and it emails a screenshot and a fix prompt when they don't. It doesn't call your Edge Functions, so a function that only fails on submit still needs a quick live test after each deploy. See what Heedza checks with the free scan.

Questions

Why do I get a CORS error when the real problem is a 500?

If the error response has no CORS headers, the browser hides it and reports CORS instead. Add corsHeaders to error responses and the real message shows up.

Is Access-Control-Allow-Origin: * safe?

It's the default in Supabase's examples and fine for most public functions that check the user's token. If the function relies on cookies or you want stricter control, list your exact origin.

Do I need CORS for server-to-server calls?

No. CORS is enforced by browsers. Calls from your server, cron jobs or Stripe webhooks don't need it.

It works locally with supabase functions serve but fails after deploy. Why?

Usually the deployed version is older than your local code, or a secret it needs isn't set in the project. Redeploy the function, check its secrets in the dashboard, and read the deployed function's logs for the real error.

Sources