Fix Supabase Edge Function "blocked by CORS policy" (preflight)
Updated · Applies to: Supabase, Lovable, Bolt, Cursor, v0
TL;DR fix: The browser sends an OPTIONS preflight before your real request, and your function doesn't answer it with CORS headers. Often it crashes first trying to req.json() an empty body. At the very top of the function, return early for OPTIONS with corsHeaders, and include corsHeaders on every success and error response. Then redeploy the function.
Steps
- Confirm it's CORS, not your logic. If the same call works from curl or Postman but fails in the browser with 'Response to preflight request doesn't pass access control check', the OPTIONS preflight is the problem. curl doesn't do CORS; browsers do.
- Handle OPTIONS before anything else. The OPTIONS check must come before req.json() or any auth or business logic. A preflight has no body, so parsing it throws and the response has no CORS headers.
- Use Supabase's corsHeaders. Import corsHeaders from the Supabase JS SDK (v2.95.0 and later) so the allowed headers match what supabase-js sends, or define them in a shared cors.ts on older versions.
- Add CORS headers to error responses too. A 400 or 500 without CORS headers shows up in the browser as a CORS error and hides the real error message.
- Redeploy the function. Run supabase functions deploy <name> (or redeploy from your builder). Edits to local files don't apply until you deploy.
The function skeleton
import { corsHeaders } from "npm:@supabase/supabase-js@^2/cors";
Deno.serve(async (req) => {
// 1. Answer the browser's preflight FIRST, before reading the body
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
try {
const { name } = await req.json();
return Response.json({ message: `Hello ${name}!` }, { headers: corsHeaders });
} catch (error) {
// 2. Errors need CORS headers too, or the browser hides them as "CORS errors"
const message = error instanceof Error ? error.message : String(error);
return Response.json({ error: message }, { status: 400, headers: corsHeaders });
}
});
On @supabase/supabase-js older than v2.95.0, Supabase’s docs say to 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",
};
Prompt for your AI tool
Browser calls to our Supabase Edge Function fail with "Response to preflight request doesn't pass access
control check". In the function: return early for OPTIONS with corsHeaders BEFORE req.json(), add corsHeaders
to every success and error response, keep the auth check. Then tell me the deploy command. When to get help
Get help if the function also handles payments, webhooks or secrets, if you need to lock Access-Control-Allow-Origin to your own domain, or if errors persist after redeploying (often an auth/JWT problem hiding behind the CORS message).
Want us to look at your app?
VibeCPR Rescue Reports are launching soon: we scan your repo and live app, and a human verifies every critical finding. Join the waitlist for early access and founder pricing. You'll also get Patch Notes, our weekly fixes newsletter.
FAQ
Why does my Supabase Edge Function work in Postman but fail in the browser with a CORS error?
Browsers send an OPTIONS preflight request before cross-origin calls with headers like Authorization and apikey. Postman and curl don't. If your function doesn't return CORS headers for OPTIONS, the browser blocks the real request. Return early for OPTIONS with corsHeaders at the top of the function.
Can I fix it by setting mode: 'no-cors' in fetch?
No. no-cors gives you an opaque response your code can't read, and supabase.functions.invoke won't work with it. Fix the headers on the function side.
Is Access-Control-Allow-Origin: * safe?
For functions that use the user's JWT for authorization it is common and matches Supabase's examples. CORS is not access control. Your function must still check who the user is. You can restrict the origin to your domain for defense in depth.
Sources
Related fixes
- Fix "Your Cloud Firestore database has insecure rules" (Firebase email)
- Fix Stripe webhook "No signatures found matching the expected signature for payload"
- Fix "new row violates row-level security policy for table" (Supabase)
- Fix "404: NOT_FOUND" on refresh in a React/Vite app on Vercel
- Fix "Module not found: Can't resolve" on Vercel (works locally)