Edge Function errors in a Lovable app: 500s, CORS, secrets
How to read Supabase Edge Function errors in a Lovable app, why a CORS error often hides a crash, and how to fix missing secrets and 401s for good.
Leer en español →
Edge Function errors in a Lovable app nearly always fall into three groups: the function crashed (a 500, often because a secret is missing), the browser blocked the answer (a CORS error, which very often is a crash in disguise), or the request was refused (a 401, usually a missing or expired sign-in token). The function's own logs tell you which one you have, so read them before changing any code.
This guide covers where those logs are, how to decode the two messages the app shows, and the fix for each group, with a small code pattern that stops CORS errors from hiding the real problem.
Where are the Edge Function logs?
It depends on which backend your Lovable project uses.
- Built-in backend (Lovable Cloud): open More → Cloud → Edge functions. Each function shows its invocations, success rate and last update. Click one, then View logs to see its output filtered to that function. You can pick a range from the last 15 minutes up to 5 days. Logs don't stream live, so click Refresh after you reproduce the error.
- Your own Supabase project: open the Supabase dashboard, go to Edge Functions, select the function and open its logs.
Reproduce the error once, refresh the logs, and look at the entry with the same time. The error text there is the real one.
What do the errors in my app mean?
Lovable apps call functions with the Supabase client, which reports failures with two different messages:
- "Edge Function returned a non-2xx status code": the function ran and answered with an error status (400, 401, 500). The reason is in the response body and in the logs.
- "Failed to send a request to the Edge Function": the browser never got a usable answer. That's a network problem, a wrong function name, or CORS.
To see the body of the first kind, read it in the calling code:
import { FunctionsHttpError } from "@supabase/supabase-js";
const { data, error } = await supabase.functions.invoke("send-invoice", { body: { id } });
if (error instanceof FunctionsHttpError) {
console.error(await error.context.json());
}
Ask Lovable to add this temporarily if you can't see what went wrong.
Why does my function return a 500?
A 500 means the code threw an error that nothing caught. The usual causes, in order:
- A missing or misnamed secret.
Deno.env.get("STRIPE_SECRET_KEY")returns nothing if the secret doesn't exist, or exists under another name, and the next line that uses it fails. - A third-party API rejected the call, for example a test key used against live data, or an expired key.
- The request body isn't what the function expects: a missing field, or a number sent as text.
- A database call failed, for example because a row-level security policy blocked it.
Fixing secrets
On Lovable Cloud, secrets live in More → Cloud → Secrets. The list shows names and creation dates; values are never shown again after saving, so if you're unsure a value is right, replace it. Check that the name matches what the code reads, letter for letter.
Two rules from Lovable's secrets docs cause a lot of confusion:
- Names starting with
SUPABASE_andLOVABLE_are reserved and filled in automatically. Your functions can readSUPABASE_URLandSUPABASE_SERVICE_ROLE_KEYwithout you adding them. - Anything starting with
VITE_is a browser value and belongs in.env, not in Secrets. Secret keys must never use that prefix.
Secrets are also not copied when you remix a project, so a remix breaks every function that needs one until you add them again.
If your app uses its own Supabase project, secrets are managed in that project's dashboard (Lovable shows a Manage secrets button that takes you there).
Why do I get a CORS error?
Browsers only accept a response from another address if it carries the right CORS headers. Supabase's CORS guide for Edge Functions asks for two things: answer the browser's OPTIONS preflight request, and include the CORS headers on every response, errors included.
The second rule is where most functions fail. If the code crashes and the runtime sends a bare 500, the browser sees no CORS headers and reports a CORS error. You go looking for a CORS problem while the actual problem is a missing secret.
The pattern that avoids this wraps the work and always returns headers:
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 result = await doTheWork(req);
return Response.json(result, { headers: corsHeaders });
} catch (err) {
console.error(err);
return Response.json({ error: String(err) }, { status: 500, headers: corsHeaders });
}
});
Older functions define corsHeaders by hand in a shared file instead of importing it. Either works, as long as every response includes them. For a broader explanation, see our guide to CORS errors.
Why do I get a 401?
By default, Supabase Edge Functions check that the request carries a valid sign-in token. A 401 means:
- the user isn't signed in, or their session expired,
- the call was made with
fetchwithout theAuthorizationheader (the Supabase client adds it for you), - or the caller isn't your app at all. Stripe, a cron service or another webhook sender doesn't have a user token.
For webhooks, the function has to accept calls without a user token and check the sender another way. In a Supabase project that's configured per function in supabase/config.toml:
[functions.stripe-webhook]
verify_jwt = false
Turning this off is only safe if the function then verifies the sender itself, for example with Stripe's signature check. Our guide on Stripe payments that don't unlock access covers that flow.
How do I confirm the fix worked?
- Trigger the feature from the published app, not only the preview.
- Refresh the function's logs: the new entry should show success, and the success rate should start climbing.
- Test the unhappy path too: a signed-out user, a bad input. You want a clear error message, not a crash.
How do I prevent these errors?
- Keep a list of every secret each function needs, and check it after a remix, a backend move or a key rotation.
- Return JSON errors with CORS headers from every function, so the app can show a useful message.
- Never put a secret key in frontend code to "get it working". If one ended up there, read what to do when an API key is exposed in your frontend.
Quick answers
Why does it work for me but not for my users? Usually the difference is the caller: your session versus a new or signed-out user, or data that only exists in your account. Compare the log entries for both calls.
Is a CORS error ever really about CORS? Sometimes, after a new header or method was added. But check the logs for a crash first.
Can I see a secret's value in Lovable? No. Secrets are write-only. Replace the value if you're unsure.
Want a second pair of eyes?
If functions fail intermittently or you're not sure your secrets and policies are safe, a code review is the fastest way to know. Start with the free health check, see how we fix Lovable apps, or describe the error and we'll come back to you.


