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

Коротко
- 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 {}". Усе інше закінчується
цим повідомленням, і вхідне значення цитується.
Уявіть колонку як сортувальну машину, що читає лише один вид етикетки: код із цих п'яти груп. Ваш застосунок поклав на стрічку посилку, на якій там, де має бути код, написане слово. Машина зупинила стрічку і прочитала вам це слово.
| Поле | Приклад | Що воно вам каже |
|---|---|---|
code | 22P02 | Код Postgres для цієї помилки, у його списку кодів він називається invalid_text_representation |
message | invalid input syntax for type uuid: "undefined" | Тип, на який чекав Postgres, і точне значення, яке він отримав |
details, hint | null | Більше нічого Postgres для цієї помилки не додає |
| Статус HTTP | 400 Bad Request | Так PostgREST, частина Supabase, яка перетворює виклики supabase.from(...) на SQL, відповідає на будь-який код без власного статусу (його код) |
Прочитайте, що стоїть у лапках
Найкорисніша частина повідомлення стоїть у лапках. Там саме те, що дійшло до бази, і кожна форма цього значення вказує на інший рядок вашого застосунку.
| У лапках | Що надіслав ваш застосунок | Де шукати |
|---|---|---|
"undefined" | змінну, яка ще не мала значення | запит, що пішов раніше, ніж завантажилися користувач або id сторінки; два наступні розділи |
"" | порожній рядок | значення, яке в стані вашого застосунку починається з '', або порожнє поле форми |
"null" | null, переданий у .eq() | .eq() теж записує null як слово. Щоб знайти рядки, де колонка порожня, документація supabase-js каже "use .is() instead" |
"[object Object]" | цілий об'єкт на місці його id | user замість user.id. JavaScript пише цей текст для будь-якого об'єкта, перетвореного на рядок |
"user_…" | id від іншого сервісу входу, не від Supabase | id у 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.
У застосунку на 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."
Чи варто змінити колонку на 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 іншого сервісу.
Чи 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. Тим часом показуйте стан завантаження.