Перейти до вмісту

Основи безпеки

Supabase "invalid input syntax for type uuid": що в лапках

Supabase "invalid input syntax for type uuid" означає, що застосунок надіслав щось, що не є id. Прочитайте лапки, притримайте запит, залиште колонку.

Vlad Tkachenko8 хв читання
Етикетка посилки з полем коду з п'яти клітинок, а перед нею бирка з бурштиновим undefined, задовга, щоб поміститися.

Коротко

  • Supabase "invalid input syntax for type uuid" (код Postgres 22P02, HTTP 400 у вашому застосунку) означає, що колонка uuid отримала значення, яке не є uuid. У лапках стоїть саме те, що надіслав ваш застосунок.
  • "undefined" у лапках означає, що запит пішов раніше, ніж з'явився id, бо supabase-js записує відсутнє значення в запит саме цим словом. Притримайте запит, доки id не з'явиться.
  • Залиште колонку типу uuid. Якщо змінити її на text, гучна помилка перетвориться на сторінку, яка мовчки нічого не показує.

Ви відкриваєте панель свого застосунку, і список проєктів якусь мить порожній, а потім заповнюється. Або лишається порожнім. Так чи так, у консолі браузера червоний рядок, а запит за ним відповів ось цим:

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

Supabase "invalid input syntax for type uuid" означає, що Postgres отримав значення, яке не є uuid, для колонки, де зберігаються лише uuid, і відхилив увесь запит. Те, що в лапках, і є тим, що надіслав ваш застосунок. Тут це слово undefined: запит пішов раніше, ніж з'явився потрібний йому id.

Швидкі виправлення, які ходять для цієї помилки, такі: змінити колонку на text або перетворити значення. Обидва прибирають повідомлення. Жодне не дає запиту його id, тож сторінка далі просить не те і перестає вам про це казати.

Що означає "invalid input syntax for type uuid"?

Це означає, що значення на шляху до колонки uuid було записане не так, як записують uuid, тож Postgres зупинився, нічого не виконавши. Жодного рядка не прочитано і жодного не змінено.

uuid має одну сталу форму: 32 символи від 0 до 9 і від a до f, записані групами по 8, 4, 4, 4 і 12, наприклад 3f2b8c1e-9a4d-4e7b-b1c2-5d6e7f8a9b0c. Supabase дає такий id кожному користувачу, що увійшов, а колонка, яка записує, чий рядок чий, як-от user_id, зберігає таке саме значення. Читач Postgres для цього типу приймає "a series of 32 hexadecimal digits with an optional dash after each group of 4 hexadecimal digits, and optionally surrounded by {}". Усе інше закінчується цим повідомленням, і вхідне значення цитується.

Уявіть колонку як сортувальну машину, що читає лише один вид етикетки: код із цих п'яти груп. Ваш застосунок поклав на стрічку посилку, на якій там, де має бути код, написане слово. Машина зупинила стрічку і прочитала вам це слово.

ПолеПрикладЩо воно вам каже
code22P02Код Postgres для цієї помилки, у його списку кодів він називається invalid_text_representation
messageinvalid input syntax for type uuid: "undefined"Тип, на який чекав Postgres, і точне значення, яке він отримав
details, hintnullБільше нічого Postgres для цієї помилки не додає
Статус HTTP400 Bad RequestТак PostgREST, частина Supabase, яка перетворює виклики supabase.from(...) на SQL, відповідає на будь-який код без власного статусу (його код)

Прочитайте, що стоїть у лапках

Найкорисніша частина повідомлення стоїть у лапках. Там саме те, що дійшло до бази, і кожна форма цього значення вказує на інший рядок вашого застосунку.

У лапкахЩо надіслав ваш застосунокДе шукати
"undefined"змінну, яка ще не мала значеннязапит, що пішов раніше, ніж завантажилися користувач або id сторінки; два наступні розділи
""порожній рядокзначення, яке в стані вашого застосунку починається з '', або порожнє поле форми
"null"null, переданий у .eq().eq() теж записує null як слово. Щоб знайти рядки, де колонка порожня, документація supabase-js каже "use .is() instead"
"[object Object]"цілий об'єкт на місці його iduser замість user.id. JavaScript пише цей текст для будь-якого об'єкта, перетвореного на рядок
"user_…"id від іншого сервісу входу, не від Supabaseid у Clerk починаються з user_ і не є uuid. Цей випадок розібрано в розділі про зміну колонки
число або назвачастину адреси сторінкиURL на кшталт /projects/42 чи /projects/my-first-app, прочитаний як id

Якщо помилка з'являється на кожному візиті, шукайте назву, під якою нічого немає. Якщо лише під час першого відмальовування сторінки, шукайте значення, яке ще не встигло завантажитися.

Чому supabase-js надсилає слово undefined?

Бо він записує кожен фільтр як текст в адресу запиту, а JavaScript записує відсутнє значення словом undefined.

Виклик на кшталт .eq('user_id', user?.id) стає ?user_id=eq., а далі значення. Вихідний код бібліотеки будує це з шаблону `eq.${value}`, а коментар нижче в тому самому файлі каже, що це робить із відсутнім значенням: воно "show up as ?column=eq.undefined". PostgREST читає все після eq. як звичайний текст, а його парсер вважає null ключовим словом лише після is.. Postgres отримує дев'ять літер слова undefined і намагається прочитати їх як uuid.

.rpc() поводиться навпаки. Його аргументи їдуть як JSON, а JSON повністю викидає значення undefined, тож виклик функції втрачає аргумент замість того, щоб надіслати слово, і Supabase відповідає Could not find the function.

supabase-js вставляє значення фільтра в запит як текст. Відсутнє значення приходить словом undefined, і колонка uuid його відхиляє.

У застосунку на React шукайте відсутнє значення у трьох місцях:

  • Користувач ще не завантажився. Supabase читає сесію асинхронно (await supabase.auth.getSession()), тож під час першого відмальовування сторінки user у стані ще порожній, а user?.id дорівнює undefined. Запит, запущений на цьому першому відмальовуванні, це й надсилає.
  • id має іншу назву. Маршрут, записаний як /projects/:projectId, кладе значення під projectId, а сторінка, яка читає id, на кожному візиті отримує undefined.
  • id береться з даних, які ще в дорозі. id з результату іншого запиту, прочитаний раніше, ніж той запит відповів.

Притримайте запит, доки не з'явиться id

Перевірте id перед викликом і дайте сторінці показувати стан завантаження, доки id не прийде. Для "undefined" і "" це і є все виправлення.

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

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

return нічого не надсилає, поки id немає, а user?.id у списку наприкінці знову запускає ефект, щойно id з'являється. На сортувальній машині це означає чекати, доки код опиниться на етикетці, і лише тоді класти посилку на стрічку.

Якщо ваш застосунок завантажує дані через TanStack Query (хук useQuery), його опція enabled працює як той самий запобіжник. Його посібник про залежні запити ставить enabled: !!userId з приміткою "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,
})

Якщо ви будуєте промптами, попросіть про це одним реченням. Залиште його англійською, мовою, якою ці інструменти міркують: "Don't run the projects query until user.id exists, and show the loading state until then."

Без перевірки перше відмальовування надсилає запит без id. З нею перше відмальовування чекає, і надсилається лише той запит, що працює.

Чи варто змінити колонку на text?

Ні, хіба що id, які ви зберігаєте, взагалі не uuid. Зміна колонки чи її перетворення на text у запиті прибирає перевірку, яка підняла помилку, і лишає відсутній id там, де він був.

Простежте той самий запит після зміни. user_id=eq.undefined тепер стає законним питанням, тож Postgres порівнює слово undefined з кожним id у колонці, не знаходить збігу і повертає порожній список. Ваша сторінка показує "Поки немає проєктів" людині, в якої їх десять, і в консолі немає нічого, що б це пояснило. Під час запису все йде далі: рядок зберігається з undefined там, де має бути id власника, і запит від справжнього власника його ніколи не знаходить. На сортувальній машині це означає вимкнути зчитувач етикеток. Посилка проходить, не збігається з жодною адресою і ніколи не доходить.

Змінювати треба саме колонку лише в одному випадку: у рядку "user_…" таблиці вище. id у Clerk текстові, і власний посібник Clerk для Supabase створює колонку як user_id text not null default auth.jwt()->>'sub'. Це інша проблема: таблицю зробили під id від Supabase, а вона отримує id іншого сервісу.

Колонка uuid відхиляє слово і каже про це. Колонка text його приймає, не знаходить жодного рядка і віддає вашій сторінці порожній список без помилки.

Чи id у вашому фільтрі тримає чужі рядки ззовні?

Ні. Фільтр вирішує, які рядки просить ваша сторінка. Які рядки відвідувачу можна читати, вирішує Row Level Security, правила для кожного рядка, які Supabase перевіряє на кожному запиті з вашим публічним ключем, і будь-хто може надіслати ваш запит знову з іншим id або зовсім без нього.

Один метод supabase-js показує різницю. .match() приймає кілька колонок одразу, і його вихідний код викидає кожну колонку зі значенням undefined, перш ніж будувати запит. Саме для цього й був той коментар про eq.undefined. Тож .match({ user_id: user?.id }), зроблений до того, як завантажився користувач, не надсилає жодного фільтра, не дає жодної помилки і повертає всі рядки, які правила таблиці дозволяють цьому відвідувачу читати. Додайте до цього виклику .single(), і він упаде інакше, з JSON object requested, multiple (or no) rows returned, бо повернулося більше одного рядка. Коли правила ввімкнені, це все одно лише рядки самого відвідувача. Коли вимкнені, це вся таблиця, а як увімкнути їх для кожної таблиці описує перемикач і перше правило.

Чи віддає таблиця рядки незнайомцеві, видно ззовні. Наше безкоштовне сканування читає ваш живий застосунок тим самим публічним ключем, який отримує відвідувач, повідомляє, які таблиці відповідають рядками, і не потребує облікового запису.

Перш ніж змінювати колонку, яку читає ваш застосунок

Збережіть копію бази з часу до зміни типу колонки і дізнайтеся, коли якась таблиця почне відповідати незнайомцям.

Reeve Care зберігає копію вашої бази даних Supabase:

  • зашифрована копія за розкладом, у власному сховищі Reeve в ЄС, поза вашим обліковим записом Supabase
  • кожна таблиця з рядками такими, якими вони були до того, як ви змінили тип колонки
  • кожна копія перевірена, перш ніж її зараховують, підрахунком рядків у кожній таблиці
  • ваші завантажені файли теж, щойно ви підключите облікові дані Storage
  • відновлення одним кліком, перед яким береться страхувальна копія поточного стану
  • усе, що робить Monitor

Reeve Monitor і далі перевіряє ззовні:

  • чи доступний ваш застосунок, з перевіркою щохвилини
  • повна перевірка безпеки щогодини, до трьох застосунків
  • повідомлення, коли результат змінюється, тож таблиця, яка після деплою почала відповідати незнайомцям, дійде до вас, і вам не доведеться йти дивитися
  • щомісячний звіт простою мовою про те, що він побачив

Що зробити зараз

Що робити

  • Прочитайте значення в лапках. Це саме те, що надіслав ваш застосунок.
  • Для "undefined" чи "" притримайте запит, доки не з'явиться id: if (!user?.id) return або enabled: !!user?.id у TanStack Query.
  • Для "null" шукайте порожні значення через .is('column', null).
  • Для "[object Object]" передавайте id об'єкта.
  • Залиште колонку типу uuid і змінюйте її на text лише тоді, коли id приходять від сервісу, що не видає uuid, як-от Clerk.
  • Якщо виклик .match() з відсутнім значенням повертає чужі рядки, увімкніть Row Level Security для цієї таблиці.

Почніть із лапок, бо значення в них вказує на рядок вашого застосунку, який його надіслав. Якщо виправлення, яке вам пропонують, змінює тип колонки, збережіть копію бази з часу до зміни.

Поширені запитання

Що означає invalid input syntax for type uuid?

Це означає, що Postgres мав прочитати значення як uuid, а значення записане не так, як uuid, тож він відхилив запит, нічого не прочитавши і не змінивши. Код 22P02, застосунок на Supabase отримує його як HTTP 400, а в лапках наприкінці повідомлення стоїть саме те, що надіслав ваш застосунок.

Чому мій id дорівнює undefined?

Бо запит пішов раніше, ніж з'явилося значення, або бо він читає назву, під якою нічого немає. Під час першого відмальовування сторінки користувача, що увійшов, ще могли не прочитати, тож user?.id дорівнює undefined, а маршрут, що називає значення projectId, щоразу дає undefined сторінці, яка читає id. supabase-js записує відсутнє значення в запит словом undefined.

Чи варто змінити колонку на text або перетворити значення?

Не для того, щоб виправити цю помилку. Колонка text приймає слово undefined, порівнює його з кожним id, не знаходить жодного, і ваша сторінка показує порожній список без жодної помилки. Змінюйте колонку, лише коли id, які ви зберігаєте, взагалі не uuid, як-от id користувачів Clerk, які власний посібник Clerk для Supabase тримає в колонці text.

Чому це стається лише під час першого завантаження?

Бо на наступному відмальовуванні id уже є, і той самий запит працює. Сторінка може заповнитися за мить і виглядати нормально, тоді як кожен візит усе одно надсилає один невдалий запит з undefined усередині. Притримайте запит, доки id не з'явиться, і цей запит не піде ніколи.

Як не дати запиту піти зарано?

Виходьте до виклику, поки id немає, наприклад рядком if (!user?.id) return на початку ефекту, і додайте user?.id до залежностей ефекту, щоб він запустився знову, щойно id з'явиться. У TanStack Query задайте enabled: !!user?.id. Тим часом показуйте стан завантаження.

Автор

Vlad Tkachenko

Засновник Reeve

Я щодня дивлюся на застосунки, зібрані в Lovable, Bolt, v0, Cursor і Replit, і на короткий список помилок, які трапляються в них знову і знову.

Більше про автора

Читати далі

Усі статті

Зробіть резервну копію бази Supabase перед наступною зміною

Reeve за розкладом зберігає зашифровану копію і відновить її за вас, якщо щось зламається.

Резервні копії Supabase

Автоматична зовнішня перевірка, а не повний аудит. Відсутність знахідок не є гарантією безпеки.