Symptoms
- A button that calls a backend feature (checkout, send email, AI reply) does nothing or shows a toast with
Failed to send a request to the Edge Function. - The browser console shows a CORS message such as
No 'Access-Control-Allow-Origin' header is present on the requested resource. - The function's own logs in Supabase show nothing, or show an error that your app never displayed.
- It may work in the Lovable or Bolt preview and fail on your live domain, or the other way round.
Why it happens
supabase-js has three Edge Function errors (source):
| Error name | Message | What it means |
|---|---|---|
FunctionsHttpError | Edge Function returned a non-2xx status code | Your function ran (or the platform answered) and returned 4xx/5xx. The body is readable. |
FunctionsRelayError | Relay Error invoking the Edge Function | Supabase's relay had a problem. |
FunctionsFetchError | Failed to send a request to the Edge Function | The fetch() call itself failed: a network error, a request blocked by CORS, or a timeout or abort. |
The third one is hard to debug because browsers deliberately hide why a cross-origin request failed. MDN says the specifics "are not available to JavaScript code" and that the only way to see them is the browser console (MDN). So supabase-js can only give you the generic message.
Supabase's own docs say a missing function causes exactly this: "Browsers misreport 404s as generic CORS errors. As a result, the Supabase JavaScript client surfaces only a generic message: Failed to send a request to the Edge Function" (Supabase, Edge Function 404).
Step-by-step fix
1. Read the real status in the Network tab (2 minutes)
On your live site, open DevTools, go to Network, filter by functions, and trigger the action again. Look at the request to https://<project-ref>.supabase.co/functions/v1/<name>:
- The
OPTIONSrequest fails, or there's noaccess-control-allow-originheader on the response. This is CORS. Go to step 2. - The request URL has the wrong project ref,
undefined, orlocalhost. This is your config. Go to step 4. - The status is 404 (sometimes shown only as "CORS error"). The function name isn't recognised. Go to step 3.
- The status is 401, 500, 503 or 504. The request did reach Supabase. Read the response body, and see Supabase's guides for 401 and 503 BOOT_ERROR. If your code returned that error without CORS headers, the browser blocks it too, so also do step 2.
You can also see the true status without a browser. Use the same public (anon or publishable) key your app uses:
curl -i -X POST "https://<project-ref>.supabase.co/functions/v1/<name>" \
-H "Authorization: Bearer <your-public-key>" \
-H "apikey: <your-public-key>" \
-H "Content-Type: application/json" \
-d '{}'A 404 with "code":"NOT_FOUND" means the function isn't deployed under that name.
2. Make the function answer CORS on every response
The function must reply to the browser's OPTIONS preflight, and it must put the same CORS headers on every response, including errors. If the function uses Supabase's withSupabase wrapper (npm:@supabase/server), the wrapper handles CORS and preflight for you (Supabase CORS guide). Otherwise add the headers yourself. With supabase-js v2.95.0 or later you can import the header list from the SDK, so it stays in sync with the headers the client sends:
import { corsHeaders } from 'npm:@supabase/supabase-js@^2/cors'
export default {
fetch: async (req: Request) => {
if (req.method === 'OPTIONS') {
return Response.json({ ok: true }, { headers: corsHeaders })
}
try {
const body = await req.json()
// ... your logic ...
return Response.json({ ok: true }, { headers: corsHeaders })
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return Response.json({ error: message }, { status: 400, headers: corsHeaders })
}
},
}If your function hardcodes Access-Control-Allow-Headers, it must list every header your app sends. If you pass a custom header in invoke(..., { headers }), add it to that list too. Otherwise the preflight fails. For a deeper look at CORS alone, see our guide on the Supabase Edge Function CORS error.
Redeploy the function after changing it.
3. Check the function name and that it's actually deployed
- The string in
supabase.functions.invoke('name')must match the deployed function exactly. Supabase lists the usual typos: wrong capitalisation, em-dashes instead of dashes, misplaced characters and unnecessary slashes (404 guide). - In the Supabase dashboard, open Edge Functions and confirm the function is there. Use its test call. If the test call works, the bug is in your app's name or URL.
- If the dashboard shows it as active but calls still 404 with
NOT_FOUND_FUNCTION_BLOB, Supabase says to redeploy it. - If you connected a different or new Supabase project, the function's code can be in your project while the function isn't deployed to the Supabase project the app actually uses. The dashboard list tells you which.
4. Check the app is talking to the right project
- Your Supabase URL and public key (for example
VITE_SUPABASE_URLandVITE_SUPABASE_ANON_KEYorVITE_SUPABASE_PUBLISHABLE_KEYin a Vite app) must be set in your production environment, and the app must be rebuilt after changing them. - If you moved from one Supabase project to another, check that the live build isn't still calling the old one. Lovable's docs also warn that two Lovable projects sharing one Supabase project overwrite each other's secrets (Lovable docs).
5. Then fix the real error
Once the request goes through, you'll usually get a normal FunctionsHttpError with a useful body. Log it like this so you can see it:
import { FunctionsHttpError } from '@supabase/supabase-js'
const { data, error } = await supabase.functions.invoke('create-checkout', { body: { priceId } })
if (error instanceof FunctionsHttpError) {
console.error('Function error:', await error.context.json())
} else if (error) {
console.error('Request never completed:', error) // FunctionsFetchError or FunctionsRelayError
}A common cause at this point is a missing secret, such as the Stripe secret key or the Resend key. Edge Function secrets live in the Supabase project, so check they're set there (Supabase: Edge Function secrets).
Also note that if you pass a timeout to invoke() and it runs out, supabase-js reports the same "Failed to send a request" message (we reproduced this with supabase-js 2.109.0).
Fix prompt for your AI builder
Paste this into Lovable, Bolt, Cursor, Replit, v0 or whichever tool you build with:
My app shows "Failed to send a request to the Edge Function" when calling supabase.functions.invoke('<FUNCTION_NAME>').
Please fix it without changing unrelated code:
1. In supabase/functions/<FUNCTION_NAME>/index.ts, handle the OPTIONS preflight first and return CORS headers on EVERY response, including error and catch responses. Use the corsHeaders export from 'npm:@supabase/supabase-js@^2/cors' if our supabase-js version is 2.95.0 or newer (or Supabase's withSupabase wrapper, which handles CORS itself); otherwise use a shared corsHeaders object that allows: authorization, x-client-info, apikey, content-type, plus any custom header we send.
2. Check that the name passed to supabase.functions.invoke() exactly matches the function folder name (same case, plain hyphens, no slashes).
3. Make sure every secret the function reads with Deno.env.get() is listed, and tell me which ones I must set in Supabase.
4. In the frontend, log error.context.json() when the error is a FunctionsHttpError so we can see the real message.
5. Tell me if the function needs to be (re)deployed to my Supabase project, and how.
Don't disable auth checks or RLS to make it work.How to confirm it's fixed
- Open your live site in an incognito window.
- Trigger the action with DevTools open on the Network tab.
- The
OPTIONSrequest returns 200 or 204 with anaccess-control-allow-originheader. ThePOSTreturns 200, or it returns an error your app now displays properly. - The function's logs in Supabase show the invocation.
How to find out sooner next time
This error tends to come back after an AI edit rewrites a function, after a key rotation, or when someone moves the app to a new Supabase project. Heedza never submits forms or presses pay buttons, so it won't trigger an invoke() that runs on click. What it does check, on a schedule, is that your landing, signup, login and checkout pages load and still show their forms and pay buttons. When one doesn't, it sends a screenshot and a fix prompt for your AI tool. The free scan also checks your homepage for exposed keys and missing security headers.
Let Heedza check this for you. Heedza’s online and page check keeps checking that your live pages open and show their forms. So if publishing a new version breaks a page, you find out. It never logs in or fills in forms, so it can’t see errors that only show after that.
Made with Lovable, Bolt, Replit, Cursor or v0? Free for 1 site: start free or see prices.