Pular para o conteúdo

Noções de segurança

Supabase "invalid input syntax for type uuid": leia as aspas

Supabase "invalid input syntax for type uuid": seu app enviou algo que não é um id. Leia as aspas, segure a consulta, mantenha a coluna.

Vlad Tkachenko10 min de leitura
Uma etiqueta de pacote com o campo do código dividido em cinco caixas e, na frente, um cartão com undefined em âmbar, comprido demais.

Em resumo

  • Supabase "invalid input syntax for type uuid" (código 22P02 do Postgres, HTTP 400 no seu app) quer dizer que uma coluna uuid recebeu um valor que não é um uuid. O texto entre aspas é exatamente o que seu app enviou.
  • "undefined" entre as aspas quer dizer que a consulta rodou antes de o id existir, porque o supabase-js escreve um valor que falta como essa palavra dentro da requisição. Segure a consulta até o id chegar.
  • Mantenha a coluna como uuid. Trocar para text transforma um erro barulhento numa página que, em silêncio, não mostra nada.

Você abre o painel do seu app e a lista de projetos fica vazia por um instante, depois se preenche. Ou fica vazia. De qualquer jeito, o console do navegador tem uma linha vermelha, e a requisição por trás respondeu com isto:

{
  "code": "22P02",
  "details": null,
  "hint": null,
  "message": "invalid input syntax for type uuid: \"undefined\""
}

Supabase "invalid input syntax for type uuid" quer dizer que o Postgres recebeu um valor que não é um uuid, para uma coluna que só guarda uuids, e recusou a requisição inteira. A parte entre aspas é o que seu app enviou. Aqui é a palavra undefined: a consulta rodou antes de existir o id de que ela precisava.

As correções rápidas que circulam para este erro são trocar a coluna para text ou converter o valor. As duas fazem a mensagem sumir. Nenhuma dá à consulta o seu id, então a página continua pedindo a coisa errada e para de avisar você.

O que significa "invalid input syntax for type uuid"?

Significa que um valor a caminho de uma coluna uuid não estava escrito do jeito que se escreve um uuid, então o Postgres parou antes de executar qualquer coisa. Nenhuma linha foi lida e nenhuma foi alterada.

Um uuid é um id num formato fixo: 32 caracteres de 0 a 9 e de a a f, escritos em grupos de 8, 4, 4, 4 e 12, como 3f2b8c1e-9a4d-4e7b-b1c2-5d6e7f8a9b0c. O Supabase dá um id assim a cada usuário logado, e uma coluna que registra de quem é cada linha, como user_id, guarda o mesmo tipo de valor. O leitor do Postgres para esse tipo aceita "a series of 32 hexadecimal digits with an optional dash after each group of 4 hexadecimal digits, and optionally surrounded by {}". Qualquer outra coisa termina nesta mensagem, com a entrada citada.

Pense na coluna como uma máquina de triagem que lê um único tipo de etiqueta: um código nesses cinco grupos. Seu app colocou na esteira um pacote com uma palavra escrita onde vai o código. A máquina parou a esteira e leu a palavra para você.

CampoExemploO que ele diz
code22P02O código do Postgres para este erro, chamado invalid_text_representation na lista de códigos dele
messageinvalid input syntax for type uuid: "undefined"O tipo que o Postgres esperava e o valor exato que recebeu
details, hintnullO Postgres não acrescenta mais nada para este erro
Status HTTP400 Bad RequestComo o PostgREST, a parte do Supabase que transforma chamadas supabase.from(...) em SQL, responde a qualquer código sem status próprio (o código dele)

Leia o que está entre as aspas

O valor citado é a metade útil da mensagem. Ele é exatamente o que chegou ao banco, e cada forma que ele assume aponta para uma linha diferente do seu app.

Entre as aspasO que seu app enviouOnde procurar
"undefined"uma variável que ainda não tinha valoruma consulta que rodou antes de o usuário ou o id da página carregar; as duas próximas seções
""uma string vaziaum valor que começa como '' no estado do seu app, ou um campo de formulário deixado em branco
"null"null, passado para .eq().eq() também escreve null como palavra. Para achar linhas em que a coluna está vazia, a documentação do supabase-js diz "use .is() instead"
"[object Object]"um objeto inteiro no lugar do id deleuser passado no lugar de user.id. O JavaScript escreve esse texto para qualquer objeto transformado em string
"user_…"um id de um serviço de login que não é o do SupabaseOs ids do Clerk começam com user_ e não são uuids. A seção sobre trocar a coluna trata desse caso
um número ou um nomeparte do endereço da páginauma URL como /projects/42 ou /projects/my-first-app, lida como id

Se o erro aparece em toda visita, procure um nome que não guarda nada. Se aparece só na primeira vez que a página é desenhada, procure um valor que ainda não tinha carregado.

Por que o supabase-js envia a palavra undefined?

Porque ele escreve cada filtro como texto no endereço da requisição, e o JavaScript escreve um valor que falta como a palavra undefined.

Uma chamada como .eq('user_id', user?.id) vira ?user_id=eq. seguido do valor. O código-fonte da biblioteca monta isso a partir de um modelo, `eq.${value}`, e um comentário mais abaixo no mesmo arquivo diz o que isso faz com um valor que falta: ele iria "show up as ?column=eq.undefined". O PostgREST lê tudo o que vem depois de eq. como texto puro, e o parser dele só trata null como palavra-chave depois de is.. O Postgres recebe as nove letras de undefined e tenta lê-las como um uuid.

.rpc() faz o contrário. Os argumentos dele viajam como JSON, que deixa de fora por completo um valor undefined, então uma chamada de função perde o argumento em vez de enviar a palavra, e o Supabase responde Could not find the function.

O supabase-js coloca o valor do filtro na requisição como texto. Um valor que falta chega como a palavra undefined, e a coluna uuid o recusa.

Num app React, procure o valor que falta em três lugares:

  • O usuário ainda não carregou. O Supabase lê a sessão logada de forma assíncrona (await supabase.auth.getSession()), então na primeira vez que sua página é desenhada, um user guardado no estado ainda está vazio e user?.id é undefined. Uma consulta disparada nesse primeiro desenho envia isso.
  • O id tem outro nome. Uma rota escrita como /projects/:projectId põe o valor em projectId, e uma página que lê id recebe undefined em toda visita.
  • O id vem de dados ainda a caminho. Um id tirado do resultado de outra consulta, lido antes de essa consulta responder.

Segure a consulta até o id existir

Confira o id antes da chamada e deixe a página mostrar o estado de carregamento até ele chegar. Para "undefined" e "", essa é a correção inteira.

useEffect(() => {
  if (!user?.id) return

  supabase
    .from('projects')
    .select('*')
    .eq('user_id', user.id)
    .then(({ data, error }) => {
      if (!error) setProjects(data)
    })
}, [user?.id])

O return não envia nada enquanto falta o id, e user?.id na lista do final roda o efeito de novo assim que o id aparece. Na máquina de triagem, é esperar o código estar na etiqueta antes de pôr o pacote na esteira.

Se seu app busca dados com TanStack Query (o hook useQuery), a opção enabled dele é a mesma trava. O guia de consultas dependentes usa enabled: !!userId com a 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,
})

Se você constrói por prompts, peça isso numa frase. Deixe em inglês, a língua em que essas ferramentas raciocinam: "Don't run the projects query until user.id exists, and show the loading state until then."

Sem a checagem, o primeiro desenho envia uma requisição sem id. Com ela, o primeiro desenho espera, e a única requisição enviada é a que funciona.

Vale trocar a coluna para text?

Não, a não ser que os ids que você guarda não sejam uuids de jeito nenhum. Trocar a coluna, ou convertê-la para text numa consulta, tira a checagem que gerou o erro e deixa o id que falta onde estava.

Acompanhe a mesma requisição depois da troca. user_id=eq.undefined agora é uma pergunta válida, então o Postgres compara a palavra undefined com cada id da coluna, não encontra nenhum e devolve uma lista vazia. Sua página mostra "Nenhum projeto ainda" para alguém que tem dez, e no console não há nada que explique. Na escrita vai além: a linha é gravada com undefined onde fica o id do dono, e uma consulta pelo dono de verdade nunca a encontra. Na máquina de triagem, é desligar o leitor de etiquetas. O pacote passa, não bate com nenhum endereço e nunca é entregue.

O único caso em que o que se troca é a coluna é a linha "user_…" da tabela acima. Os ids do Clerk são texto, e o próprio guia do Clerk para Supabase cria a coluna como user_id text not null default auth.jwt()->>'sub'. É outro problema: a tabela foi feita para os ids do Supabase e está recebendo os de outro serviço.

Uma coluna uuid recusa a palavra e avisa. Uma coluna text aceita, não encontra linha nenhuma e entrega à sua página uma lista vazia sem erro.

É o id do seu filtro que mantém as linhas dos outros de fora?

Não. O filtro decide quais linhas sua página pede. Quais linhas um visitante pode ler quem decide é a Row Level Security, as regras por linha que o Supabase confere em cada requisição feita com sua chave pública, e qualquer pessoa pode reenviar sua requisição com outro id ou sem nenhum.

Um método do supabase-js mostra a diferença. .match() recebe várias colunas de uma vez, e o código-fonte dele tira qualquer coluna cujo valor seja undefined antes de montar a requisição. Era para isso aquele comentário sobre eq.undefined. Então um .match({ user_id: user?.id }) feito antes de o usuário carregar não envia filtro nenhum, não gera erro nenhum e devolve todas as linhas que as regras da tabela deixam esse visitante ler. Acrescente .single() a essa chamada e ela falha de outro jeito, com JSON object requested, multiple (or no) rows returned, porque voltou mais de uma linha. Com as regras ligadas, essas linhas continuam sendo só as do visitante. Com elas desligadas, são a tabela inteira, e ligar as regras em toda tabela explica o interruptor e a primeira regra.

Dá para ver de fora se uma tabela entrega linhas a um estranho. Nosso scan gratuito lê seu app no ar com a mesma chave pública que um visitante recebe, informa quais tabelas respondem com linhas e não pede conta.

Antes de mudar uma coluna que seu app lê

Guarde uma cópia do banco de antes de mudar o tipo de uma coluna, e fique sabendo quando uma tabela começar a responder a estranhos.

Reeve Care guarda uma cópia do seu banco no Supabase:

  • uma cópia criptografada num cronograma, no armazenamento próprio da Reeve na UE, fora da sua conta do Supabase
  • cada tabela com as linhas como estavam antes de você mudar o tipo de uma coluna
  • cada cópia verificada antes de valer, contando as linhas de cada tabela
  • seus arquivos enviados também, assim que você conecta uma credencial do Storage
  • uma restauração com um clique, com uma cópia de segurança do estado atual feita antes
  • tudo o que o Monitor faz

Reeve Monitor continua conferindo de fora:

  • se seu app está no ar, conferido a cada minuto
  • a verificação de segurança completa a cada hora, em até três apps
  • uma mensagem quando um resultado muda, para que uma tabela que começa a responder a estranhos depois de um deploy chegue até você sem você ter que ir olhar
  • um relatório mensal, em linguagem simples, do que ele viu

O que fazer agora

O que fazer

  • Leia o valor entre as aspas. Ele é exatamente o que seu app enviou.
  • Para "undefined" ou "", segure a consulta até o id existir: if (!user?.id) return, ou enabled: !!user?.id no TanStack Query.
  • Para "null", encontre valores vazios com .is('column', null).
  • Para "[object Object]", passe o id do objeto.
  • Mantenha a coluna como uuid e troque para text só quando os ids vierem de um serviço que não emite uuids, como o Clerk.
  • Se uma chamada .match() com um valor faltando devolver linhas de outras pessoas, ligue a Row Level Security nessa tabela.

Comece pelas aspas, porque o valor dentro delas aponta para a linha do seu app que o enviou. Se a correção que propõem a você muda o tipo de uma coluna, guarde uma cópia do banco de antes da mudança.

Perguntas frequentes

O que significa invalid input syntax for type uuid?

Significa que o Postgres tinha de ler um valor como uuid e o valor não está escrito como um, então ele recusou a requisição antes de ler ou mudar qualquer coisa. O código é 22P02, um app Supabase o recebe como HTTP 400, e o texto entre aspas no fim da mensagem é exatamente o que seu app enviou.

Por que meu id está undefined?

Porque a consulta rodou antes de o valor existir, ou porque ela lê um nome que não guarda nada. Na primeira vez que uma página é desenhada, o usuário logado pode ainda não ter sido lido, então user?.id é undefined, e uma rota que chama o valor de projectId entrega undefined, sempre, a uma página que lê id. O supabase-js escreve o valor que falta na requisição como a palavra undefined.

Devo trocar a coluna para text ou converter o valor?

Não para corrigir isso. Uma coluna text aceita a palavra undefined, compara com cada id, não encontra nenhum, e sua página mostra uma lista vazia sem erro nenhum. Troque a coluna só quando os ids que você guarda não são uuids, como os ids de usuário do Clerk, que o próprio guia do Clerk para Supabase guarda numa coluna text.

Por que só acontece no primeiro carregamento?

Porque no desenho seguinte o id já chegou e a mesma consulta funciona. A página pode se preencher um instante depois e parecer certa, enquanto cada visita ainda envia uma requisição com falha e undefined dentro. Segure a consulta até o id existir e essa requisição nunca é enviada.

Como impeço a consulta de rodar cedo demais?

Saia antes da chamada enquanto o id faltar, por exemplo com if (!user?.id) return no topo do efeito, e coloque user?.id como dependência do efeito para ele rodar de novo quando o id aparecer. Com TanStack Query, use enabled: !!user?.id. Enquanto isso, mostre seu estado de carregamento.

Escrito por

Vlad Tkachenko

Fundador da Reeve

Passo meus dias olhando apps criados com Lovable, Bolt, v0, Cursor e Replit, e a curta lista de erros que aparecem neles sem parar.

Mais sobre o autor

Leia depois

Todos os artigos

Faça backup do seu banco Supabase antes da próxima mudança

O Reeve guarda uma cópia criptografada em horários programados e a restaura para você se algo quebrar.

Ver backups do Supabase

Verificação externa automatizada, não uma auditoria completa. A ausência de achados não é garantia de segurança.