Short answer
The function has to answer the browser’s OPTIONS preflight request and include CORS headers on every response, errors included. Supabase’s wrapper does this for you, or you can import corsHeaders from supabase-js. If it still fails, read the response status: an error can make the browser report CORS when the real problem is something else.
The usual causes, and the fix
01
No answer to the preflight request
Browsers send an OPTIONS preflight request before cross-origin calls that use headers such as Authorization or a JSON Content-Type. The function must answer it with CORS headers. Supabase’s docs show two ways: the withSupabase wrapper from @supabase/server, which handles CORS and the preflight for you, or corsHeaders imported from supabase-js v2.95.0 and later (earlier versions need a shared cors.ts) plus a check for OPTIONS at the top of the handler.
import { corsHeaders } from 'npm:@supabase/supabase-js@^2/cors'; Deno.serve(async (req) => { if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }); } try { const data = { ok: true }; return Response.json(data, { headers: corsHeaders }); } catch (error) { return Response.json({ error: 'Bad request' }, { status: 400, headers: corsHeaders }); } });02
Headers missing on error responses
If only the success path adds the headers, any error comes back without them and the browser reports CORS instead of the real error. Supabase notes that an error before the CORS check can make the browser blame CORS. Add the headers to every response, and read the function’s logs in the dashboard to find the actual error. A missing OPTIONS handler can show up there as SyntaxError: Unexpected end of JSON input.
03
The platform rejects the request before your code runs
With JWT verification on (the default), the platform checks the Authorization header first. If it’s missing, malformed or signed with a different key, the platform returns 401 and your code never runs, so none of your headers are added. Call the function with supabase.functions.invoke, which sends the header. Supabase also now recommends turning off the built-in check and verifying the user in your own code.
Checked against the vendor’s docs on : Supabase docs: CORS support for Edge Functions; Supabase docs: Edge Function auth headers; Supabase troubleshooting: Edge Function 500 error responses; Supabase troubleshooting: Edge Function 401 error responses; MDN: Preflight request.
Rather hand it over?
Fixed scope, fixed price, paid upfront. The work happens in writing; no call needed.
- from $450App Fix TicketOne reproducible bug in an existing mobile or web app, diagnosed and fixed.See scope and start
- from $450AI-Built App RescueA Lovable, Bolt or Cursor app checked end to end in 3 days, then its worst problems fixed at a fixed price.No issues worth fixing, no charge. If the Launch Check finds nothing rated medium or higher, Simon refunds the full $450. You keep the report either way.See scope and start
Related: Supabase emails link to localhost · Supabase RLS fixes