Skip to main content

Supabase Edge Function CORS error: answer the preflight and keep the headers on errors

Why the browser blocks your function, how to find the real cause, and the code that fixes it.

Updated 3 min readMarkdown

Your Edge Function works when you test it in the Supabase dashboard. Then your app calls it from the browser and the console says "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".

What's going on: before the real request, the browser sends an OPTIONS preflight and checks the response for CORS headers. If the function doesn't answer the preflight, or answers without the headers, the browser stops right there.

Four reasons it fails

  • The preflight isn't handled. The function runs its normal code on OPTIONS, tries to read a body that isn't there, and errors out without headers.
  • Only the success path has headers. A thrown error or a 400 goes back bare, so the browser shows CORS instead of your actual error.
  • The client sends a header you didn't allow. For example x-client-info or a trace header that isn't in Access-Control-Allow-Headers.
  • The function never started. A boot crash returns no headers at all, and CORS is the only symptom you see.

Pin down which one it is

  1. Network tab: open the OPTIONS request and look at its status and response headers.
  2. Supabase dashboard: read the function's logs. Crashes and thrown errors are logged there, not in the browser.
  3. curl: call the function directly. If it fails there as well, fix the function first; CORS isn't the problem.

The fix: SDK headers on every response

If you're on supabase-js 2.95.0 or later, import corsHeaders from the SDK so they always match what the client sends. Answer OPTIONS with them, and return them from the success and error paths alike. Then redeploy.

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

Deno.serve(async (req) => {
  if (req.method === 'OPTIONS') {
    return Response.json({ ok: true }, { headers: corsHeaders })
  }
  try {
    const { name } = await req.json()
    return Response.json({ message: 'Hello ' + name }, { headers: corsHeaders })
  } catch (error) {
    const message = error instanceof Error ? error.message : 'Bad request'
    return Response.json({ error: message }, { status: 400, headers: corsHeaders })
  }
})

Supabase's docs also offer a withSupabase wrapper that takes care of CORS and the preflight if you'd rather not manage headers yourself.

Before launch: narrow the origin

A wildcard Access-Control-Allow-Origin is fine for public functions. For anything that acts on a signed-in user, return your own domain.

How to confirm it's fixed

  • The OPTIONS request returns 200 with Access-Control-Allow-Origin.
  • Send a bad body on purpose: you should see your own error message, not a CORS error.

Keep it from coming back

  • Put the headers in one shared helper that every function uses.
  • When the browser says CORS, read the function logs first. It's often a crash wearing a CORS mask.

Why does curl work when the browser doesn't? curl doesn't enforce CORS or send a preflight; only browsers do.

Do webhooks need CORS? No. Webhooks come from servers such as Stripe, which ignore CORS. They need signature checks instead.

Source: Supabase: CORS support for Edge Functions.


The original fix is on our site: Fix CORS errors when calling a Supabase Edge Function. If you're getting ready to launch, the free Launch Check looks at a live app in about a minute.

I wrote this with AI assistance and reviewed it myself.

About the author

  • Founder and CEO of StacksMesh (stacksmesh.com). We fix, launch and migrate apps built with Base44, Lovable, Bolt and Replit, with a fixed price in writing before any work. I write about making AI-built apps safe and production-ready.

No comments yet. Be the first to comment.