Saltar al contenido

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.

Vlad Tkachenko10 min de lectura
Una etiqueta de paquete con una casilla de código partida en cinco cajas y, delante, una tarjeta con undefined en ámbar, demasiado larga.

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.

CampoEjemploLo que te dice
code22P02El código de Postgres para este error, llamado invalid_text_representation en su lista de códigos
messageinvalid input syntax for type uuid: "undefined"El tipo que Postgres esperaba y el valor exacto que recibió
details, hintnullPostgres no añade nada más para este error
Estado HTTP400 Bad RequestCó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 comillasLo que envió tu appDónde mirar
"undefined"una variable que aún no tenía valoruna consulta que se ejecutó antes de que cargara el usuario o el id de la página; las dos secciones siguientes
""una cadena vacíaun 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 iduser 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 SupabaseLos 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 nombreparte de la dirección de la páginauna 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.

supabase-js mete el valor del filtro en la petición como texto. Un valor que falta llega como la palabra undefined, y la columna uuid lo rechaza.

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, un user guardado en el estado sigue vacío y user?.id es undefined. Una consulta lanzada en ese primer dibujado lo envía.
  • El id tiene otro nombre. Una ruta escrita como /projects/:projectId pone el valor bajo projectId, y una página que lee id recibe undefined en 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."

Sin la comprobación, el primer dibujado envía una petición sin id. Con ella, el primer dibujado espera, y la única petición enviada es la que funciona.

¿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.

Una columna uuid rechaza la palabra y lo dice. Una columna text la acepta, no encuentra ninguna fila y le entrega a tu página una lista vacía sin error.

¿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, o enabled: !!user?.id en TanStack Query.
  • Con "null", encuentra los valores vacíos con .is('column', null).
  • Con "[object Object]", pasa el id del objeto.
  • Deja la columna como uuid y cámbiala a text solo 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.

Escrito por

Vlad Tkachenko

Fundador de Reeve

Me paso el día mirando apps creadas con Lovable, Bolt, v0, Cursor y Replit, y la corta lista de errores que aparecen en ellas una y otra vez.

Más sobre el autor

Sigue leyendo

Todos los artículos

Haz una copia de tu base de datos de Supabase antes del próximo cambio

Reeve la guarda cifrada con regularidad y la restaura por ti si algo falla.

Ver copias de Supabase

Comprobación externa automatizada, no una auditoría completa. La ausencia de hallazgos no es garantía de seguridad.