Errores de Edge Functions en Lovable: 500, CORS y secretos
Cómo leer los errores de las Edge Functions de Supabase en una app de Lovable, por qué un error de CORS suele esconder una falla y cómo arreglar secretos y 401.
Read in English →
Los errores de Edge Functions en una app de Lovable caen casi siempre en tres grupos: la función se cayó (un 500, muchas veces porque falta un secreto), el navegador bloqueó la respuesta (un error de CORS, que muy seguido es una caída disfrazada) o la petición fue rechazada (un 401, casi siempre por un token de sesión que falta o venció). Los registros de la propia función te dicen en cuál estás, así que léelos antes de cambiar una sola línea.
Esta guía explica dónde están esos registros, cómo interpretar los dos mensajes que muestra la app y cómo resolver cada grupo, con un patrón de código corto que evita que el CORS tape el problema real.
¿Dónde veo los registros de una Edge Function?
Depende del backend de tu proyecto.
- Backend integrado (Lovable Cloud): entra a More → Cloud → Edge functions. Cada función muestra sus invocaciones, el porcentaje de éxito y la última actualización. Elige una y haz clic en View logs para ver su salida filtrada. Puedes mirar desde los últimos 15 minutos hasta 5 días atrás. Los registros no se actualizan solos: después de reproducir el error, haz clic en Refresh.
- Tu propio proyecto de Supabase: en el panel de Supabase, entra a Edge Functions, elige la función y abre sus registros.
Reproduce el error una vez, actualiza los registros y busca la entrada con la misma hora. El texto de error que aparece ahí es el verdadero.
¿Qué significan los errores que muestra mi app?
Las apps de Lovable llaman a las funciones con el cliente de Supabase, que informa las fallas con dos mensajes distintos:
- "Edge Function returned a non-2xx status code": la función se ejecutó y respondió con un código de error (400, 401, 500). La razón está en el cuerpo de la respuesta y en los registros.
- "Failed to send a request to the Edge Function": el navegador nunca recibió una respuesta útil. Es un problema de red, un nombre de función equivocado o CORS.
Para ver el cuerpo del primer caso, léelo en el código que hace la llamada:
import { FunctionsHttpError } from "@supabase/supabase-js";
const { data, error } = await supabase.functions.invoke("enviar-factura", { body: { id } });
if (error instanceof FunctionsHttpError) {
console.error(await error.context.json());
}
Si no logras ver qué falló, pídele a Lovable que lo agregue de forma temporal.
¿Por qué mi función devuelve un 500?
Un 500 quiere decir que el código lanzó un error que nadie atrapó. Las causas típicas, en orden:
- Un secreto que falta o tiene otro nombre.
Deno.env.get("STRIPE_SECRET_KEY")no devuelve nada si el secreto no existe o se guardó con otro nombre, y la siguiente línea que lo usa falla. - Una API externa rechazó la llamada: una clave de prueba usada con datos reales o una clave vencida.
- El cuerpo de la petición no es el que espera la función: falta un campo o llega un número como texto.
- Falló una consulta a la base, por ejemplo porque una política de row level security la bloqueó.
Cómo arreglar los secretos
En Lovable Cloud, los secretos están en More → Cloud → Secrets. La lista muestra nombres y fechas de creación; los valores no se vuelven a mostrar después de guardarlos, así que si dudas de uno, reemplázalo. Revisa que el nombre coincida letra por letra con el que lee el código.
Dos reglas de la documentación de secretos de Lovable generan mucha confusión:
- Los nombres que empiezan con
SUPABASE_yLOVABLE_están reservados y se completan solos. Tus funciones ya pueden leerSUPABASE_URLySUPABASE_SERVICE_ROLE_KEYsin que los agregues. - Lo que empieza con
VITE_es un valor para el navegador y va en.env, no en Secrets. Una clave secreta nunca debe llevar ese prefijo.
Además, los secretos no se copian al hacer remix de un proyecto, así que un remix rompe todas las funciones que dependen de uno hasta que los vuelvas a cargar.
Si tu app usa su propio proyecto de Supabase, los secretos se administran en ese panel (Lovable muestra un botón Manage secrets que te lleva ahí).
¿Por qué me sale un error de CORS?
El navegador solo acepta respuestas de otra dirección si traen los encabezados de CORS correctos. La guía de CORS para Edge Functions de Supabase pide dos cosas: responder a la petición previa OPTIONS del navegador e incluir los encabezados de CORS en todas las respuestas, también en las de error.
La segunda regla es donde fallan casi todas. Si el código se cae y el servidor devuelve un 500 sin esos encabezados, el navegador no los ve y reporta un error de CORS. Terminas buscando un problema de CORS cuando el problema real es un secreto que falta.
Este patrón lo evita: envuelve el trabajo y siempre devuelve los encabezados.
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 resultado = await hacerElTrabajo(req);
return Response.json(resultado, { headers: corsHeaders });
} catch (err) {
console.error(err);
return Response.json({ error: String(err) }, { status: 500, headers: corsHeaders });
}
});
Las funciones más viejas definen corsHeaders a mano en un archivo compartido en vez de importarlo. Cualquiera de las dos formas sirve, siempre que todas las respuestas los lleven. Para entender el tema a fondo, mira la guía sobre el error de CORS.
¿Por qué me sale un 401?
Por defecto, las Edge Functions de Supabase verifican que la petición traiga un token de sesión válido. Un 401 significa que:
- el usuario no inició sesión o su sesión venció,
- la llamada se hizo con
fetchsin el encabezadoAuthorization(el cliente de Supabase lo agrega solo), - o quien llama no es tu app: Stripe, un servicio de tareas programadas u otro webhook no tienen token de usuario.
Para los webhooks, la función tiene que aceptar llamadas sin token de usuario y comprobar el remitente de otra forma. En un proyecto de Supabase se configura por función en supabase/config.toml:
[functions.stripe-webhook]
verify_jwt = false
Desactivarlo solo es seguro si la función verifica al remitente por su cuenta, por ejemplo con la firma de Stripe. Ese flujo lo explicamos en pagos con Stripe en una app hecha con IA.
¿Cómo confirmo que el arreglo funcionó?
- Dispara la función desde la app publicada, no solo desde la vista previa.
- Actualiza los registros: la entrada nueva debe salir como exitosa y el porcentaje de éxito debe empezar a subir.
- Prueba también el caso malo: un usuario sin sesión, un dato inválido. Lo que quieres es un mensaje claro, no una caída.
¿Cómo prevengo estos errores?
- Lleva una lista de los secretos que necesita cada función y revísala después de un remix, una mudanza del backend o una rotación de claves.
- Haz que todas las funciones devuelvan errores en JSON con encabezados de CORS, para que la app pueda mostrar un mensaje útil.
- Nunca pongas una clave secreta en el frontend "para que funcione". Si ya pasó, lee qué hacer con una clave de API expuesta en el frontend.
Respuestas rápidas
¿Por qué a mí me funciona y a mis usuarios no? Casi siempre cambia quién llama: tu sesión frente a un usuario nuevo o sin sesión, o datos que solo existen en tu cuenta. Compara las entradas de registro de las dos llamadas.
¿Un error de CORS alguna vez es realmente de CORS? A veces, después de agregar un encabezado o un método nuevo. Pero primero busca una caída en los registros.
¿Puedo ver el valor de un secreto en Lovable? No. Los secretos solo se escriben. Si dudas, reemplaza el valor.
¿Quieres que alguien más lo revise?
Si las funciones fallan de forma intermitente o no estás seguro de que tus secretos y políticas sean seguros, una revisión del código es la forma más rápida de saberlo. Empieza con el chequeo gratuito de tu app, mira cómo arreglamos apps de Lovable o cuéntanos el error.


