CORS error calling an Edge Function
Fix "blocked by CORS policy" when calling a SupaHost Edge Function from the browser: answer OPTIONS preflights, add headers to every response, and spot router errors hiding behind CORS.
What you see
In the browser console:
Access to fetch at 'https://app-one.acme.supahost.space/functions/v1/send-invoice'
from origin 'http://localhost:5173' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
supabase-js reports it as a FunctionsFetchError ("Failed to send a request to the Edge Function"). The same call works with curl or from a server.
Why it happens
Browsers only let a page read a response from another origin if the response says it's allowed. Before a POST with JSON or an Authorization header, the browser sends an OPTIONS preflight request. Your function must answer it, and every real response must carry the CORS headers too. Edge Functions don't add these for you.
Three common slips:
- The function doesn't handle
OPTIONSat all. - The success path sets headers, but error responses (
400,500) don't, so failures show up as CORS errors. Access-Control-Allow-Headersdoesn't list a header the client sends (supabase-js sendsauthorization,x-client-info,apikeyandcontent-type).
How to fix it
Create a shared headers file at
_shared/cors.tsin the function's files:export const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', 'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS', }For a private tool, replace
*with your app's origin, such ashttps://invoices.example.com.Answer preflights and add the headers everywhere:
import { corsHeaders } from './_shared/cors.ts' Deno.serve(async (req) => { if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }) } try { const { invoiceId } = await req.json() return Response.json({ sent: invoiceId }, { headers: corsHeaders }) } catch (err) { console.error(err) return Response.json({ error: 'Bad request' }, { status: 400, headers: corsHeaders }) } })Recent versions of supabase-js (2.95.0 and later) also export a ready-made
corsHeadersobject from@supabase/supabase-js/cors.Add any custom headers your app sends (for example
x-tenant-id) toAccess-Control-Allow-Headers.Deploy and retry. Check DevTools → Network: the
OPTIONSrequest should return200with the headers, followed by your real request.
On SupaHost
OPTIONS requests skip the Require JWT check, so preflights reach your code without a token. But the router's own error responses, such as 401 for a missing JWT or 404 for an unknown slug, are produced before your code runs, so they don't carry the headers your code sets. When a browser gets such a response without an Access-Control-Allow-Origin header, it reports a CORS error that is really an auth or URL problem. In DevTools, look at the real request's status: if it's 401 or 404 with an sb-error-code header, fix that first. See Edge Function returns 401 and Edge Function returns 404.