Fundamentos de seguridad
"Edge Function returned a non-2xx status code": el error real
"Edge Function returned a non-2xx status code" significa que tu función de Supabase respondió con un error. Aquí ves dónde leer el real.

En resumen
- "Edge Function returned a non-2xx status code" es lo que supabase-js dice de cualquier respuesta de tu función fuera de los 200. La razón real llegó en la misma respuesta, y los logs de la función guardan el resto.
- Lee primero el número de estado. 401 significa que la petición se rechazó antes de que tu código corriera, 404 que no hay ninguna función desplegada con ese nombre, 500 que tu código falló, 546 o 504 que se quedó sin recursos o sin tiempo.
- Dos arreglos rápidos hacen callar el mensaje y abren un agujero: llamar a la API desde tu frontend con la clave dentro, y apagar la verificación JWT sin poner un control propio en su lugar.
Pulsas en tu app el botón que llama a una Edge Function y, en lugar de un resultado, tu app muestra esto:
Edge Function returned a non-2xx status code
Es la misma frase pase lo que pase. Una clave que falta, una función que se cayó, una petición rechazada antes de que tu código corriera: todas dicen "Edge Function returned a non-2xx status code", así que el mensaje por sí solo casi no te dice qué arreglar.
Lo que no te dice es que la razón ya llegó. El error volvió en la misma respuesta que esa frase, y supabase-js no te lo lee. Buscar la frase te da las suposiciones de otros. Leer la respuesta te da la tuya.
¿Qué significa "Edge Function returned a non-2xx status code"?
Tu función respondió, y la respuesta fue un error. Eso es todo lo que dice la frase.
Cada respuesta en la web lleva un código de estado, un número de tres cifras que
dice cómo fue la petición. Cualquier cosa en los 200 significa que funcionó.
Cuando tu app llama a una función con supabase.functions.invoke, supabase-js
mira ese número y, si está fuera de los 200, le entrega a tu app una frase fija
en lugar de los datos. La frase está escrita palabra por palabra en
el propio supabase-js,
y el error que la lleva se llama FunctionsHttpError.
Piensa en la llamada como una carta que envías a una oficina interna. La oficina contesta lo que pediste o te devuelve la carta. Cuando vuelve, supabase-js lee el sello del sobre y te dice que fue devuelta. No abre el sobre. La nota de dentro dice por qué, y la oficina llevó un registro de todo lo que pasó mientras tenía tu carta.
Dos parientes cercanos se parecen y significan otra cosa. Failed to send a request to the Edge Function significa que no volvió ninguna respuesta legible,
y en un navegador eso suele ser una cabecera CORS que falta, algo que cubre
el artículo sobre ese mensaje.
Y si varias partes sin relación de tu app se rompieron a la vez, comprueba
si es Supabase o tu app antes de abrir
ningún código.
¿Dónde está el error real?
En dos sitios, y puedes leer los dos sin cambiar una línea de código.
La respuesta misma. Abre tu app publicada en Chrome, Edge o Firefox, pulsa
F12, elige la pestaña Network y repite lo que falló. Haz clic en la petición
cuya dirección contiene /functions/v1/ seguido del nombre de tu función. Su
columna Status tiene el número, y su pestaña Response tiene lo que la
función envió con él: un mensaje que escribió tu propio código, un contenido
JSON corto de la plataforma de Supabase, o las palabras Internal Server Error.
Si puedes cambiar la llamada, o pedírselo a la herramienta que escribe tu código, la
guía de manejo de errores de Supabase
muestra cómo leer esa misma respuesta dentro de la app, porque la respuesta
original viaja con el error como error.context:
const { data, error } = await supabase.functions.invoke('your-function', { body })
if (error) console.log(error.context.status, await error.context.text())
El registro de la función. En el panel de Supabase, abre Edge Functions,
elige la función y lee dos pestañas. Invocations lista cada petición con su
código de estado y lo que tardó. Logs guarda lo que la
guía de logs de Supabase
llama eventos de plataforma, excepciones no capturadas y mensajes de log
propios, es decir, todo lo que tu código imprimió con console.log o
console.error. Reproduce el fallo una vez, anota la hora y lee las dos
pestañas en ese minuto.
Lee primero el código de estado
El número te dice hasta dónde llegó tu petición antes de que la devolvieran, y eso decide dónde está escrita la razón.
| Estado | Qué pasó | Dónde está la razón |
|---|---|---|
401 | Rechazada antes de que tu código corriera, o tu código la rechazó | La respuesta. Invalid JWT o Missing authorization header es la plataforma |
404 | No hay ninguna función con ese nombre desplegada en este proyecto | La respuesta dice Requested function was not found |
500 | Tu código corrió y lanzó un error, o devolvió un 500 a propósito | La pestaña Logs |
503 | La función ni siquiera arrancó | La pestaña Logs: un import, un error de sintaxis o la configuración de la función |
504 | Ninguna respuesta en 150 segundos | La pestaña Invocations, y luego lo que tu código estaba esperando |
546 | Se quedó sin tiempo de CPU o sin memoria | La pestaña Logs dice cuál |
| Cualquier otro 4xx | Lo eligió tu propio código, por ejemplo al pasar la respuesta de un servicio | La respuesta |
Los significados vienen de la referencia de códigos de estado de Supabase y de su página de límites. Un 404 no necesita más que esta tabla: compara el nombre de la función en tu app con la lista del panel, y comprueba que esté desplegada en el proyecto con el que habla tu app. Las demás filas tienen cada una una sección más abajo.
500: tu función corrió y falló
Algo en tu código lanzó un error, y la pestaña Logs tiene la línea.
La página de Supabase sobre este error
lo divide en dos. Si la respuesta dice exactamente Internal Server Error, tu
código se cayó y el log muestra el tipo de error y la línea donde ocurrió, por
ejemplo TypeError: Cannot read properties of undefined. Si la respuesta dice
otra cosa, tu código escribió ese mensaje y devolvió el 500 a propósito, así que
busca en la función el estado que envió.
Hay una causa que conviene descartar antes que ninguna otra en una función que llama a OpenAI, Stripe o cualquier API de pago: un secreto que existe en tu máquina y no en tu proyecto publicado.
Dentro de la función, un secreto se lee con Deno.env.get('OPENAI_API_KEY').
Cuando no hay ningún secreto con ese nombre, la documentación de Deno dice que
esto devuelve undefined en
lugar de fallar. Así que la función sigue, arma una petición con la palabra
undefined donde debería ir la clave y la envía. OpenAI responde con su propio
401, "Incorrect API key provided", y tu función lo pasa tal cual o se cae al
leer una respuesta que no esperaba. En la imagen del sobre: la oficina abrió tu
carta, fue al cajón donde debía estar la clave, lo encontró vacío y devolvió una
nota sobre una clave equivocada.
El arreglo está en la página Edge Function Secrets de tu proyecto de
Supabase. La
guía de secretos de Supabase
dice que una función lee un secreto nuevo al instante, así que no vuelves a
desplegar, y que ningún nombre de secreto puede empezar por SUPABASE_, porque
ese prefijo está reservado para los valores que Supabase define por ti. Un
secreto que defines para el desarrollo local se queda en tu máquina; el proyecto
publicado tiene su propia lista.
401: rechazada antes de que tu código corriera
Si la respuesta dice Invalid JWT o Missing authorization header, tu función
nunca vio la petición. La plataforma de Supabase la rechazó en la puerta.
Esa puerta es una verificación llamada verify_jwt, activa por defecto, que lee
la cabecera Authorization de cada petición antes de que corra tu código. La
página de Supabase sobre el 401
da tres maneras de no pasarla:
- La clave no coincide con las de tu proyecto. Por ejemplo una clave copiada de otro proyecto, o una vieja que sigue puesta en algún sitio después de una rotación.
- No hay cabecera
Authorizationen absoluto. supabase-js añade una por ti. Unfetchsimple a la dirección de la función, o un webhook de otro servicio, no lo hace. - Tu proyecto usa las claves de firma nuevas de Supabase. La verificación integrada solo entiende el formato antiguo, y la solución de Supabase es apagar la verificación para esa función.
Ese último punto va más allá de un caso. Supabase recomienda ahora apagar la verificación integrada y gestionar la autenticación directamente en el código de tu función. La recomendación tiene dos mitades, y la segunda es la que se queda por el camino.
Si el 401 llegó con un mensaje que escribió tu propio código, la plataforma dejó
pasar la petición y tu función la rechazó, o pasó un 401 del servicio al que
llamaba, que a menudo es el secreto vacío de la sección del 500. Además es una
verificación distinta de la que hay detrás de PGRST301 en tus consultas a la
base de datos, que tiene su propia página. Si el
registro de la función muestra invalid claim: missing sub claim, preguntó a
Supabase Auth quién llamaba y recibió la clave de tu proyecto en lugar de un
usuario con sesión iniciada, algo que
el artículo de ese mensaje sigue paso
a paso.
546 y 504: sin recursos o sin tiempo
Supabase detiene una función que consume demasiado, y los dos números dicen qué límite tocó.
546 es cómputo. La
página de límites da a una
función 256 MB de memoria y a cada petición 2 segundos de tiempo de CPU. Pasa de
cualquiera de los dos y la respuesta dice
Function failed due to not having enough compute resources (please check logs),
con el código WORKER_RESOURCE_LIMIT (los logs antiguos dicen WORKER_LIMIT).
La pestaña Logs dice CPU Time exceeded o Memory limit exceeded. La
página de Supabase sobre el 546
pone como ejemplos el procesamiento de imágenes, los embeddings de IA y el web
scraping, y su respuesta es hacer menos en cada petición o pasar la parte pesada
a otro servicio.
504 es tiempo. Una función que no envía ninguna respuesta en 150 segundos recibe un 504. El límite de CPU solo cuenta el tiempo en que tu código está calculando de verdad: la página de límites dice que no incluye la entrada y salida asíncrona, y esperar a la red es justo eso. Así que una función que espera a un modelo de IA lento no gasta CPU. Va consumiendo el reloj de 150 segundos, y cuando se acaba recibes un 504 y no un 546.
Dos arreglos que lo empeoran
Dos cambios hacen que este mensaje desaparezca en minutos, y los dos te dejan peor que el error.
Llamar a la API desde la página. La forma más rápida de que una función que
falla deje de fallar es dejar de usarla: poner la clave en una variable VITE_
y llamar a OpenAI directamente desde tu frontend. Funciona al primer intento.
También pone la clave en el archivo que descarga cada visitante, porque ese
prefijo es
una instrucción para compilar el valor dentro de tu bundle,
y ahí cualquiera puede leerla. Entre las apps publicadas que hemos escaneado,
apareció una clave de OpenAI en el JavaScript de 33, y
lo que un desconocido puede hacer con una
tiene su propio artículo.
Apagar la verificación y quedarse ahí. Quitarse un 401 apagando
verify_jwt es la primera mitad del consejo de la propia Supabase. La
guía de configuración de Supabase
deja claro lo que eso hace por sí solo: cualquiera puede entonces invocar tu
Edge Function sin un JWT válido. La dirección de tu función está en tu frontend
para que cualquiera la lea. Si la función guarda una clave de OpenAI, un
desconocido puede gastarla ahora a través de ti. Si usa la clave secreta del
proyecto, que Supabase pone en el entorno de cada función, la petición de un
desconocido corre con una clave que se salta todas las reglas de row level
security que escribiste.
La segunda mitad, un control dentro de la función sobre quién llama, son unas pocas líneas. Mover un secreto a una Edge Function las tiene, y explica por qué incluso la verificación por defecto deja pasar a más gente de lo que su nombre sugiere.
El primer error se ve desde fuera, y así lo contamos. Nuestro escaneo gratuito lee el JavaScript de tu app publicada como lo haría un desconocido y nombra cada clave que encuentra, indicando si es de las que deben estar ahí. Tarda unos 20 segundos y no necesita cuenta: escanea tu app.
Que la clave se quede donde la pusiste
El arreglo de hoy lo puede deshacer el prompt de la semana que viene. Un builder al que pides que vuelva a hacer funcionar algo es libre de tomar el atajo de arriba, y tu app sigue funcionando mientras lo hace.
Reeve Monitor lo revisa cada hora por ti:
- las nueve comprobaciones cada hora, en hasta tres apps
- un correo el día en que un nuevo escaneo califica tu app peor que el anterior; una clave secreta en tu JavaScript limita por sí sola la nota a D
- si la app está disponible, cada 60 segundos
- un informe mensual de lo que vio
Una función que corre con tu clave secreta se salta todas las reglas de row level security. Reeve Care guarda una copia de tu base de datos de Supabase donde nada de tu proyecto puede alcanzarla.
- una copia cifrada según un calendario, guardada en el almacenamiento propio de Reeve en la UE, fuera de tu cuenta de Supabase
- cada copia verificada antes de contar, contando las filas de cada tabla
- una restauración en un clic que guarda lo que hay ahora antes de reemplazarlo
- también tus archivos subidos, en cuanto conectes una credencial de Storage
- todo lo que hace Monitor
Qué hacer ahora
Qué hacer
- Reproduce el fallo una vez y anota la hora.
- Abre la petición en la pestaña Network de tu navegador y lee su estado y su respuesta antes de cambiar nada.
- Lee las pestañas Invocations y Logs de la función en el panel de Supabase en ese minuto.
- Ante un 500 de una función que llama a una API de pago, comprueba que el secreto esté definido en la página Edge Function Secrets del proyecto publicado. Después no hace falta volver a desplegar.
- Ante un 401 de la plataforma, corrige la clave o apaga la verificación integrada, y añade en el mismo cambio un control de quién llama dentro de la función.
- Nunca quites este error moviendo la clave a una variable
VITE_.
El control de quién llama es el paso más fácil de saltarse, y la guía de secretos en Edge Functions lo cubre entero. La checklist de seguridad de 10 minutos cubre lo que suele quedar abierto en una app recién lanzada.
Preguntas frecuentes
¿Qué significa "Edge Function returned a non-2xx status code"?
Significa que tu Edge Function de Supabase respondió con un estado de error, cualquier número fuera de los 200, y supabase-js convirtió esa respuesta en una frase fija. La frase es la misma para un fallo del código, una clave que falta, una petición rechazada antes de que tu código corriera y un tiempo de espera agotado. La respuesta en sí, que dice cuál de esos fue, sigue pegada al error, y la función guarda un log propio.
¿Cómo veo el error real que hay detrás?
Abre tu app publicada, pulsa F12, elige la pestaña Network y repite lo que falló. Haz clic en la petición cuya dirección contiene /functions/v1/ y lee su estado y su respuesta: eso es lo que la función envió de verdad. Después abre Edge Functions en tu panel de Supabase, elige la función y lee las pestañas Invocations y Logs a la misma hora. En código, la guía de Supabase lee esa misma respuesta con await error.context.json().
¿Por qué mi Edge Function funciona en local y falla al desplegarla?
Comprueba primero si un secreto existe en tu máquina y no en tu proyecto publicado. El entorno local lee su propio archivo de entorno, y la función desplegada lee los secretos definidos en el proyecto. Un secreto que falta vuelve como undefined en lugar de como un error, así que la función sigue y falla más adelante, normalmente cuando el servicio al que llama rechaza una clave vacía. Defínelo en la página Edge Function Secrets; Supabase indica que un secreto nuevo se lee al instante, sin volver a desplegar.
¿Debo apagar la verificación JWT para arreglar un 401?
Supabase recomienda ahora apagar la verificación integrada y comprobar quién llama dentro de tu función. Las dos mitades cuentan. Apagada sola, la verificación ya no detiene nada, y la propia documentación de Supabase advierte que así cualquiera puede invocar tu función sin un JWT válido. Si la función guarda una clave de OpenAI o la clave secreta del proyecto, eso es cualquiera en internet gastando tu cuota o saltándose tus reglas de row level security.
¿Qué significa el error 546 en una Edge Function de Supabase?
La función se quedó sin cómputo: más de 2 segundos de tiempo de CPU, o más que su memoria, en una petición. La respuesta dice "Function failed due to not having enough compute resources (please check logs)", y la pestaña Logs dice qué límite fue, CPU Time exceeded o Memory limit exceeded. Esperar a una API lenta no cuenta como tiempo de CPU; una función que espera demasiado recibe un 504 en su lugar.