Fundamentos de seguridad
Supabase "invalid input syntax for type uuid": lee las comillas
Supabase "invalid input syntax for type uuid" significa que tu app envió algo que no es un id. Lee las comillas, frena la consulta, conserva la columna.

En resumen
- Supabase "invalid input syntax for type uuid" (código 22P02 de Postgres, HTTP 400 en tu app) significa que una columna uuid recibió un valor que no es un uuid. El texto entre comillas es exactamente lo que envió tu app.
- "undefined" entre las comillas significa que la consulta se ejecutó antes de que existiera el id, porque supabase-js escribe un valor que falta como esa palabra dentro de la petición. Frena la consulta hasta que el id esté.
- Deja la columna como uuid. Cambiarla a text convierte un error ruidoso en una página que, en silencio, no muestra nada.
Abres el panel de tu app y la lista de proyectos se queda vacía un momento y luego se llena. O se queda vacía. En cualquier caso, la consola del navegador tiene una línea roja, y la petición que hay detrás respondió con esto:
{
"code": "22P02",
"details": null,
"hint": null,
"message": "invalid input syntax for type uuid: \"undefined\""
}
Supabase "invalid input syntax for type uuid" significa que Postgres recibió un
valor que no es un uuid, para una columna que solo guarda uuids, y rechazó la
petición entera. Lo que va entre comillas es lo que envió tu app. Aquí es la
palabra undefined: la consulta se ejecutó antes de que existiera el id que
necesitaba.
Los arreglos rápidos que circulan para este error son cambiar la columna a text
o convertir el valor. Los dos hacen desaparecer el mensaje. Ninguno le da a la
consulta su id, así que la página sigue pidiendo lo que no es y deja de avisarte.
¿Qué significa "invalid input syntax for type uuid"?
Significa que un valor que iba hacia una columna uuid no estaba escrito como se
escribe un uuid, así que Postgres se detuvo antes de ejecutar nada. No se leyó
ninguna fila y no se cambió ninguna.
Un uuid es un id con una forma fija: 32 caracteres del 0 al 9 y de la a a
la f, escritos en grupos de 8, 4, 4, 4 y 12, como
3f2b8c1e-9a4d-4e7b-b1c2-5d6e7f8a9b0c. Supabase da a cada usuario con sesión un
id así, y una columna que registra de quién es cada fila, como user_id, guarda
el mismo tipo de valor. El
lector de Postgres para ese tipo
acepta "a series of 32 hexadecimal digits with an optional dash after each group
of 4 hexadecimal digits, and optionally surrounded by {}". Cualquier otra cosa
termina en este mensaje, con la entrada citada.
Piensa en la columna como una máquina clasificadora que solo lee un tipo de etiqueta: un código en esos cinco grupos. Tu app puso en la cinta un paquete con una palabra escrita donde va el código. La máquina paró la cinta y te leyó la palabra.
| Campo | Ejemplo | Lo que te dice |
|---|---|---|
code | 22P02 | El código de Postgres para este error, llamado invalid_text_representation en su lista de códigos |
message | invalid input syntax for type uuid: "undefined" | El tipo que Postgres esperaba y el valor exacto que recibió |
details, hint | null | Postgres no añade nada más para este error |
| Estado HTTP | 400 Bad Request | Cómo responde PostgREST, la parte de Supabase que convierte las llamadas supabase.from(...) en SQL, a cualquier código sin un estado propio (su código) |
Lee lo que hay entre las comillas
El valor citado es la mitad útil del mensaje. Es exactamente lo que llegó a la base de datos, y cada forma que toma apunta a una línea distinta de tu app.
| Entre las comillas | Lo que envió tu app | Dónde mirar |
|---|---|---|
"undefined" | una variable que aún no tenía valor | una consulta que se ejecutó antes de que cargara el usuario o el id de la página; las dos secciones siguientes |
"" | una cadena vacía | un valor que empieza como '' en el estado de tu app, o un campo de formulario en blanco |
"null" | null, pasado a .eq() | .eq() también escribe null como palabra. Para encontrar filas en las que la columna está vacía, la documentación de supabase-js dice "use .is() instead" |
"[object Object]" | un objeto entero donde iba su id | user en lugar de user.id. JavaScript escribe ese texto para cualquier objeto convertido en cadena |
"user_…" | un id de un servicio de inicio de sesión que no es el de Supabase | Los ids de Clerk empiezan por user_ y no son uuids. La sección sobre cambiar la columna trata este caso |
| un número o un nombre | parte de la dirección de la página | una URL como /projects/42 o /projects/my-first-app, leída como id |
Si el error aparece en cada visita, busca un nombre que no contiene nada. Si solo aparece la primera vez que se dibuja la página, busca un valor que aún no había cargado.
¿Por qué supabase-js envía la palabra undefined?
Porque escribe cada filtro como texto en la dirección de la petición, y
JavaScript escribe un valor que falta como la palabra undefined.
Una llamada como .eq('user_id', user?.id) se convierte en ?user_id=eq.
seguido del valor. El
código fuente de la librería
lo construye con una plantilla, `eq.${value}`, y un comentario más abajo en
el mismo archivo dice qué hace eso con un valor que falta: "show up as
?column=eq.undefined". PostgREST lee todo lo que sigue a eq. como texto
plano, y
su parser
solo trata null como palabra clave después de is.. Postgres recibe las nueve
letras de undefined e intenta leerlas como un uuid.
.rpc() va al revés. Sus argumentos viajan como JSON, que deja fuera por completo
un valor undefined, así que una llamada a una función pierde el argumento en vez
de enviar la palabra, y Supabase responde
Could not find the function.
En una app de React, busca el valor que falta en tres sitios:
- El usuario aún no ha cargado. Supabase lee la sesión iniciada de forma
asíncrona (
await supabase.auth.getSession()), así que la primera vez que se dibuja tu página, unuserguardado en el estado sigue vacío yuser?.idesundefined. Una consulta lanzada en ese primer dibujado lo envía. - El id tiene otro nombre. Una ruta escrita como
/projects/:projectIdpone el valor bajoprojectId, y una página que leeidrecibeundefineden cada visita. - El id viene de datos que aún están en camino. Un id sacado del resultado de otra consulta, leído antes de que esa consulta haya respondido.
Frena la consulta hasta que exista el id
Comprueba el id antes de la llamada y deja que la página muestre su estado de
carga hasta que llegue. Para "undefined" y "" ese es todo el arreglo.
useEffect(() => {
if (!user?.id) return
supabase
.from('projects')
.select('*')
.eq('user_id', user.id)
.then(({ data, error }) => {
if (!error) setProjects(data)
})
}, [user?.id])
El return no envía nada mientras falta el id, y user?.id en la lista del
final vuelve a ejecutar el efecto en cuanto aparece. En la máquina clasificadora,
eso es esperar a que el código esté en la etiqueta antes de poner el paquete en
la cinta.
Si tu app carga datos con TanStack Query (el hook useQuery), su opción
enabled es el mismo freno.
Su guía de consultas dependientes
pone enabled: !!userId con la nota "The query will not execute until the userId
exists".
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: async () => {
const { data, error } = await supabase
.from('projects')
.select('*')
.eq('user_id', user.id)
if (error) throw error
return data
},
enabled: !!user?.id,
})
Si construyes con prompts, pídelo en una frase. Déjala en inglés, que es el idioma en el que piensan estas herramientas: "Don't run the projects query until user.id exists, and show the loading state until then."
¿Deberías cambiar la columna a text?
No, salvo que los ids que guardas no sean uuids en absoluto. Cambiar la columna, o convertirla a text en una consulta, quita la comprobación que lanzó el error y deja el id que falta donde estaba.
Sigue la misma petición después del cambio. user_id=eq.undefined ahora es una
pregunta válida, así que Postgres compara la palabra undefined con cada id de la
columna, no encuentra ninguna coincidencia y devuelve una lista vacía. Tu página
muestra "Aún no hay proyectos" a alguien que tiene diez, y en la consola no hay
nada que lo explique. Al escribir va más lejos: la fila se guarda con undefined
donde va el id de su dueño, y una consulta del dueño real no la encuentra nunca.
En la máquina clasificadora, eso es desenchufar el lector de etiquetas. El
paquete pasa, no coincide con ninguna dirección y nunca se entrega.
El único caso en que lo que hay que cambiar es la columna es la fila "user_…"
de la tabla de arriba. Los ids de Clerk son texto, y
la propia guía de Clerk para Supabase
crea la columna como user_id text not null default auth.jwt()->>'sub'. Es otro
problema: la tabla se hizo para los ids de Supabase y está recibiendo los de otro
servicio.
¿Es el id de tu filtro lo que deja fuera las filas ajenas?
No. El filtro decide qué filas pide tu página. Qué filas puede leer un visitante lo decide Row Level Security, las reglas por fila que Supabase comprueba en cada petición hecha con tu clave pública, y cualquiera puede volver a enviar tu petición con otro id o sin ninguno.
Un método de supabase-js muestra la diferencia. .match() recibe varias columnas
a la vez, y
su código fuente
quita cualquier columna cuyo valor sea undefined antes de construir la petición.
Para eso estaba aquel comentario sobre eq.undefined. Así que un
.match({ user_id: user?.id }) hecho antes de que cargue el usuario no envía
ningún filtro, no da ningún error y devuelve todas las filas que las reglas de la
tabla le dejan leer a ese visitante. Añade .single() a esa llamada y falla de
otra manera, con
JSON object requested, multiple (or no) rows returned,
porque volvió más de una fila. Con las reglas activadas, esas filas siguen siendo
solo las del visitante. Con ellas desactivadas, son la tabla entera, y
activarlas en cada tabla explica el
interruptor y la primera regla.
Si una tabla le entrega filas a un desconocido se puede ver desde fuera. Nuestro escaneo gratuito lee tu app en vivo con la misma clave pública que recibe un visitante, informa de qué tablas responden con filas y no necesita cuenta.
Antes de cambiar una columna que lee tu app
Guarda una copia de la base de datos de antes de cambiar el tipo de una columna, y entérate cuando una tabla empiece a responder a desconocidos.
Reeve Care guarda una copia de tu base de datos de Supabase:
- una copia cifrada según un calendario, en el almacenamiento propio de Reeve en la UE, fuera de tu cuenta de Supabase
- cada tabla con sus filas tal como estaban antes de que cambiaras el tipo de una columna
- cada copia verificada antes de contar, contando las filas de cada tabla
- también tus archivos subidos, en cuanto conectas una credencial de Storage
- una restauración con un clic, con una copia de seguridad del estado actual tomada antes
- todo lo que hace Monitor
Reeve Monitor sigue comprobando desde fuera:
- si tu app está en línea, comprobado cada minuto
- la comprobación de seguridad completa cada hora, en hasta tres apps
- un mensaje cuando un resultado cambia, para que una tabla que empieza a responder a desconocidos tras un despliegue te llegue sin que tengas que ir a mirar
- un informe mensual, en lenguaje claro, de lo que vio
Qué hacer ahora
Qué hacer
- Lee el valor entre las comillas. Es exactamente lo que envió tu app.
- Con
"undefined"o"", frena la consulta hasta que exista el id:if (!user?.id) return, oenabled: !!user?.iden TanStack Query. - Con
"null", encuentra los valores vacíos con.is('column', null). - Con
"[object Object]", pasa eliddel objeto. - Deja la columna como
uuidy cámbiala atextsolo cuando los ids vengan de un servicio que no emite uuids, como Clerk. - Si una llamada
.match()con un valor que falta devuelve filas de otras personas, activa Row Level Security en esa tabla.
Empieza por las comillas, porque el valor que contienen apunta a la línea de tu app que lo envió. Si el arreglo que te proponen cambia el tipo de una columna, guarda una copia de la base de datos de antes del cambio.
Preguntas frecuentes
¿Qué significa invalid input syntax for type uuid?
Significa que Postgres tenía que leer un valor como uuid y el valor no está escrito como uno, así que rechazó la petición antes de leer o cambiar nada. El código es 22P02, una app de Supabase lo recibe como HTTP 400, y el texto entre comillas al final del mensaje es exactamente lo que envió tu app.
¿Por qué mi id es undefined?
Porque la consulta se ejecutó antes de que existiera el valor, o porque lee un nombre que no contiene nada. La primera vez que se dibuja una página puede que el usuario con sesión aún no se haya leído, así que user?.id es undefined, y una ruta que llama al valor projectId le da undefined, siempre, a una página que lee id. supabase-js escribe el valor que falta en la petición como la palabra undefined.
¿Debo cambiar la columna a text o convertir el valor?
No para arreglar esto. Una columna text acepta la palabra undefined, la compara con cada id, no encuentra ninguno, y tu página muestra una lista vacía sin ningún error. Cambia la columna solo cuando los ids que guardas no son uuids, como los ids de usuario de Clerk, que la propia guía de Clerk para Supabase guarda en una columna text.
¿Por qué solo pasa en la primera carga?
Porque en el siguiente dibujado el id ya llegó y la misma consulta funciona. La página puede llenarse un momento después y parecer correcta, mientras cada visita sigue enviando una petición fallida con undefined dentro. Frena la consulta hasta que exista el id y esa petición no se envía nunca.
¿Cómo evito que la consulta se ejecute demasiado pronto?
Sal antes de la llamada mientras falte el id, por ejemplo con if (!user?.id) return al principio del efecto, y pon user?.id como dependencia del efecto para que vuelva a ejecutarse cuando aparezca el id. Con TanStack Query, usa enabled: !!user?.id. Mientras tanto, muestra tu estado de carga.