Aller au contenu

Bases de la sécurité

Supabase "invalid input syntax for type uuid" : lisez les guillemets

Supabase "invalid input syntax for type uuid" : votre app a envoyé autre chose qu'un id. Lisez les guillemets, retenez la requête, gardez la colonne.

Vlad Tkachenko10 min de lecture
Une étiquette de colis dont le code tient en cinq cases, et devant, une étiquette volante portant undefined en ambre, trop longue.

En bref

  • Supabase "invalid input syntax for type uuid" (code Postgres 22P02, HTTP 400 dans votre application) signifie qu'une colonne uuid a reçu une valeur qui n'est pas un uuid. Le texte entre guillemets est exactement ce que votre application a envoyé.
  • "undefined" entre les guillemets signifie que la requête est partie avant que l'id existe, car supabase-js écrit une valeur manquante sous ce mot dans la requête. Retenez la requête jusqu'à ce que l'id soit là.
  • Gardez la colonne en uuid. La passer en text transforme une erreur bruyante en une page qui, sans bruit, n'affiche rien.

Vous ouvrez le tableau de bord de votre application, et la liste des projets reste vide un instant, puis se remplit. Ou elle reste vide. Dans les deux cas, la console du navigateur affiche une ligne rouge, et la requête derrière a répondu ceci :

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

Supabase "invalid input syntax for type uuid" signifie que Postgres a reçu une valeur qui n'est pas un uuid, pour une colonne qui ne contient que des uuid, et qu'il a refusé toute la requête. La partie entre guillemets est ce que votre application a envoyé. Ici, c'est le mot undefined : la requête est partie avant que l'id dont elle avait besoin existe.

Les correctifs rapides qui circulent pour cette erreur consistent à passer la colonne en text ou à convertir la valeur. Les deux font disparaître le message. Aucun ne donne son id à la requête, donc la page continue de demander la mauvaise chose et cesse de vous le dire.

Que signifie "invalid input syntax for type uuid" ?

Cela signifie qu'une valeur destinée à une colonne uuid n'était pas écrite comme on écrit un uuid, donc Postgres s'est arrêté avant d'exécuter quoi que ce soit. Aucune ligne n'a été lue, aucune n'a été modifiée.

Un uuid est un id de forme fixe : 32 caractères de 0 à 9 et de a à f, écrits en groupes de 8, 4, 4, 4 et 12, comme 3f2b8c1e-9a4d-4e7b-b1c2-5d6e7f8a9b0c. Supabase donne un id de ce genre à chaque utilisateur connecté, et une colonne qui note à qui appartient chaque ligne, comme user_id, contient le même genre de valeur. Le lecteur de Postgres pour ce type accepte "a series of 32 hexadecimal digits with an optional dash after each group of 4 hexadecimal digits, and optionally surrounded by {}". Tout le reste finit par ce message, avec l'entrée citée.

Imaginez la colonne comme une trieuse qui ne lit qu'un seul type d'étiquette : un code en ces cinq groupes. Votre application a posé sur le tapis un colis avec un mot écrit à la place du code. La machine a arrêté le tapis et vous a lu le mot.

ChampExempleCe qu'il vous dit
code22P02Le code de Postgres pour cette erreur, appelé invalid_text_representation dans sa liste de codes
messageinvalid input syntax for type uuid: "undefined"Le type que Postgres attendait, et la valeur exacte qu'il a reçue
details, hintnullPostgres n'ajoute rien d'autre pour cette erreur
Statut HTTP400 Bad RequestLa réponse de PostgREST, la partie de Supabase qui traduit les appels supabase.from(...) en SQL, à tout code sans statut propre (son code)

Lisez ce qu'il y a entre les guillemets

La valeur citée est la moitié utile du message. C'est exactement ce qui est arrivé dans la base, et chaque forme qu'elle prend désigne une ligne différente de votre application.

Entre les guillemetsCe que votre application a envoyéOù regarder
"undefined"une variable qui n'avait pas encore de valeurune requête partie avant que l'utilisateur ou l'id de la page soit chargé ; les deux sections suivantes
""une chaîne videune valeur qui démarre à '' dans l'état de votre application, ou un champ de formulaire laissé vide
"null"null, passé à .eq().eq() écrit aussi null comme un mot. Pour trouver les lignes où la colonne est vide, la documentation de supabase-js dit "use .is() instead"
"[object Object]"un objet entier à la place de son iduser passé au lieu de user.id. JavaScript écrit ce texte pour tout objet converti en chaîne
"user_…"un id venant d'un service de connexion autre que celui de SupabaseLes id de Clerk commencent par user_ et ne sont pas des uuid. La section sur le changement de colonne traite ce cas
un nombre ou un nomune partie de l'adresse de la pageune URL comme /projects/42 ou /projects/my-first-app, lue comme l'id

Si l'erreur apparaît à chaque visite, cherchez un nom sous lequel il n'y a rien. Si elle n'apparaît qu'au premier affichage de la page, cherchez une valeur qui n'était pas encore chargée.

Pourquoi supabase-js envoie-t-il le mot undefined ?

Parce qu'il écrit chaque filtre sous forme de texte dans l'adresse de la requête, et que JavaScript écrit une valeur manquante sous la forme du mot undefined.

Un appel comme .eq('user_id', user?.id) devient ?user_id=eq. suivi de la valeur. Le code source de la bibliothèque le construit à partir d'un modèle, `eq.${value}`, et un commentaire plus bas dans le même fichier dit ce que cela fait d'une valeur manquante : elle "show up as ?column=eq.undefined". PostgREST lit tout ce qui suit eq. comme du texte brut, et son analyseur ne traite null comme un mot-clé qu'après is.. Postgres reçoit les neuf lettres de undefined et essaie de les lire comme un uuid.

.rpc() fait l'inverse. Ses arguments voyagent en JSON, qui omet complètement une valeur undefined, si bien qu'un appel de fonction perd l'argument au lieu d'envoyer le mot, et Supabase répond Could not find the function.

supabase-js place la valeur du filtre dans la requête sous forme de texte. Une valeur manquante arrive sous la forme du mot undefined, et la colonne uuid la refuse.

Dans une application React, cherchez la valeur manquante à trois endroits :

  • L'utilisateur n'est pas encore chargé. Supabase lit la session connectée de façon asynchrone (await supabase.auth.getSession()), donc au premier affichage de votre page, un user gardé dans l'état est encore vide et user?.id vaut undefined. Une requête lancée à ce premier affichage l'envoie.
  • L'id porte un autre nom. Une route écrite /projects/:projectId range la valeur sous projectId, et une page qui lit id reçoit undefined à chaque visite.
  • L'id vient de données encore en route. Un id tiré du résultat d'une autre requête, lu avant que cette requête ait répondu.

Retenez la requête jusqu'à ce que l'id existe

Vérifiez l'id avant l'appel et laissez la page afficher son état de chargement jusqu'à son arrivée. Pour "undefined" et "", c'est toute la correction.

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

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

Le return n'envoie rien tant que l'id manque, et user?.id dans la liste à la fin relance l'effet dès que l'id apparaît. Sur la trieuse, c'est attendre que le code soit sur l'étiquette avant de poser le colis sur le tapis.

Si votre application charge ses données avec TanStack Query (le hook useQuery), son option enabled est le même verrou. Son guide des requêtes dépendantes règle enabled: !!userId avec la note "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 vous construisez par prompts, demandez-le en une phrase. Laissez-la en anglais, la langue dans laquelle ces outils raisonnent : "Don't run the projects query until user.id exists, and show the loading state until then."

Sans la vérification, le premier affichage envoie une requête sans id. Avec elle, le premier affichage attend, et la seule requête envoyée est celle qui fonctionne.

Faut-il passer la colonne en text ?

Non, sauf si les id que vous stockez ne sont pas du tout des uuid. Changer la colonne, ou la convertir en text dans une requête, retire la vérification qui a levé l'erreur et laisse l'id manquant là où il était.

Suivez la même requête après le changement. user_id=eq.undefined est désormais une question valide, donc Postgres compare le mot undefined à chaque id de la colonne, ne trouve aucune correspondance et renvoie une liste vide. Votre page affiche "Aucun projet pour l'instant" à quelqu'un qui en a dix, et la console ne contient rien pour l'expliquer. À l'écriture, cela va plus loin : la ligne est enregistrée avec undefined à la place de l'id de son propriétaire, et une requête pour le vrai propriétaire ne la trouve jamais. Sur la trieuse, c'est débrancher le lecteur d'étiquettes. Le colis passe, ne correspond à aucune adresse et n'est jamais livré.

Le seul cas où c'est la colonne qu'il faut changer est la ligne "user_…" du tableau plus haut. Les id de Clerk sont du texte, et le guide Supabase de Clerk lui-même crée la colonne avec user_id text not null default auth.jwt()->>'sub'. C'est un autre problème : la table a été faite pour les id de Supabase et reçoit ceux d'un autre service.

Une colonne uuid refuse le mot et le dit. Une colonne text l'accepte, ne trouve aucune ligne et remet à votre page une liste vide sans erreur.

L'id de votre filtre tient-il les lignes des autres à l'écart ?

Non. Le filtre décide quelles lignes votre page demande. Quelles lignes un visiteur a le droit de lire, c'est Row Level Security qui le décide, les règles par ligne que Supabase vérifie à chaque requête faite avec votre clé publique, et n'importe qui peut renvoyer votre requête avec un autre id ou sans aucun.

Une méthode de supabase-js montre la différence. .match() prend plusieurs colonnes à la fois, et son code source retire toute colonne dont la valeur est undefined avant de construire la requête. C'est à cela que servait ce commentaire sur eq.undefined. Un .match({ user_id: user?.id }) fait avant le chargement de l'utilisateur n'envoie donc aucun filtre, ne lève aucune erreur et renvoie toutes les lignes que les règles de la table laissent lire à ce visiteur. Ajoutez .single() à cet appel et il échoue autrement, avec JSON object requested, multiple (or no) rows returned, parce que plus d'une ligne est revenue. Règles activées, ces lignes restent seulement celles du visiteur. Règles désactivées, c'est toute la table, et les activer sur chaque table décrit l'interrupteur et la première règle.

On peut voir de l'extérieur si une table donne des lignes à un inconnu. Notre scan gratuit lit votre application en ligne avec la même clé publique qu'un visiteur, indique quelles tables répondent avec des lignes, et ne demande aucun compte.

Avant de changer une colonne que lit votre application

Gardez une copie de la base d'avant le changement de type d'une colonne, et soyez prévenu quand une table se met à répondre à des inconnus.

Reeve Care garde une copie de votre base de données Supabase :

  • une copie chiffrée selon un calendrier, sur le stockage propre de Reeve dans l'UE, en dehors de votre compte Supabase
  • chaque table avec ses lignes telles qu'elles étaient avant que vous changiez le type d'une colonne
  • chaque copie vérifiée avant de compter, en comptant les lignes de chaque table
  • vos fichiers envoyés aussi, dès que vous connectez un identifiant Storage
  • une restauration en un clic, avec une copie de sécurité de l'état actuel prise d'abord
  • tout ce que fait Monitor

Reeve Monitor continue de vérifier de l'extérieur :

  • si votre application répond, vérifié chaque minute
  • la vérification de sécurité complète toutes les heures, sur trois applications au plus
  • un message quand un résultat change, pour qu'une table qui se met à répondre à des inconnus après un déploiement vous parvienne sans que vous alliez voir
  • un rapport mensuel, en langage clair, de ce qu'il a vu

Que faire maintenant

Que faire

  • Lisez la valeur entre les guillemets. C'est exactement ce que votre application a envoyé.
  • Pour "undefined" ou "", retenez la requête jusqu'à ce que l'id existe : if (!user?.id) return, ou enabled: !!user?.id dans TanStack Query.
  • Pour "null", trouvez les valeurs vides avec .is('column', null).
  • Pour "[object Object]", passez l'id de l'objet.
  • Gardez la colonne en uuid, et ne la passez en text que si les id viennent d'un service qui ne délivre pas d'uuid, comme Clerk.
  • Si un appel .match() avec une valeur manquante renvoie les lignes d'autres personnes, activez Row Level Security sur cette table.

Commencez par les guillemets, car la valeur qu'ils contiennent désigne la ligne de votre application qui l'a envoyée. Si la correction qu'on vous propose change le type d'une colonne, gardez une copie de la base d'avant le changement.

FAQ

Que signifie invalid input syntax for type uuid ?

Postgres devait lire une valeur comme un uuid et la valeur n'est pas écrite comme un uuid, donc il a refusé la requête avant de lire ou de modifier quoi que ce soit. Le code est 22P02, une application Supabase le reçoit en HTTP 400, et le texte entre guillemets à la fin du message est exactement ce que votre application a envoyé.

Pourquoi mon id vaut-il undefined ?

Parce que la requête est partie avant que la valeur existe, ou parce qu'elle lit un nom sous lequel il n'y a rien. Au premier affichage d'une page, l'utilisateur connecté n'a peut-être pas encore été lu, donc user?.id vaut undefined, et une route qui appelle la valeur projectId donne undefined à chaque fois à une page qui lit id. supabase-js écrit la valeur manquante dans la requête sous la forme du mot undefined.

Faut-il passer la colonne en text ou convertir la valeur ?

Pas pour corriger ceci. Une colonne text accepte le mot undefined, le compare à chaque id, n'en trouve aucun, et votre page affiche une liste vide sans la moindre erreur. Ne changez la colonne que si les id que vous stockez ne sont pas des uuid, comme les id d'utilisateur de Clerk, que le guide Supabase de Clerk lui-même range dans une colonne text.

Pourquoi cela n'arrive-t-il qu'au premier chargement ?

Parce qu'à l'affichage suivant l'id est arrivé et la même requête fonctionne. La page peut se remplir un instant plus tard et sembler correcte, alors que chaque visite envoie quand même une requête en échec avec undefined dedans. Retenez la requête jusqu'à ce que l'id existe et cette requête n'est jamais envoyée.

Comment empêcher la requête de partir trop tôt ?

Sortez avant l'appel tant que l'id manque, par exemple avec if (!user?.id) return en haut de l'effet, et mettez user?.id dans les dépendances de l'effet pour qu'il se relance dès que l'id apparaît. Avec TanStack Query, réglez enabled: !!user?.id. Affichez votre état de chargement en attendant.

Écrit par

Vlad Tkachenko

Fondateur de Reeve

Je passe mes journées à examiner des applications créées avec Lovable, Bolt, v0, Cursor et Replit, et la courte liste d'erreurs qui y reviennent sans cesse.

En savoir plus sur l'auteur

À lire ensuite

Tous les articles

Sauvegardez votre base Supabase avant la prochaine modification

Reeve en garde une copie chiffrée à intervalles réguliers et la restaure pour vous en cas de problème.

Voir les sauvegardes Supabase

Vérification externe automatisée, pas un audit complet. L'absence de problème n'est pas une garantie de sécurité.