Основи безпеки
"Failed to send a request to the Edge Function": справжня причина
"Failed to send a request to the Edge Function": браузер не отримав відповіді, яку можна прочитати. Його консоль каже, чому, і чи ваша функція запускалася.

Коротко
- "Failed to send a request to the Edge Function" означає, що supabase-js не отримав жодної відповіді, яку йому дозволено прочитати. Або запит не пройшов дорогою, або браузер затримав відповідь, і ваш застосунок навмисно не дізнається більше.
- Причина є в консолі браузера. Рядок, де згадано preflight request, означає, що ваш запит так і не пішов. Рядок без цієї згадки означає, що ваша функція відпрацювала, а затримали лише її відповідь.
- Виправлення: гілка для OPTIONS на самому початку функції та заголовки CORS у кожній відповіді, з помилками включно. Ці заголовки лише кажуть браузерам, хто може прочитати відповідь; хто може викликати функцію, вирішує перевірка у вашому коді.
У вашому застосунку є кнопка, яка викликає Supabase Edge Function, той невеликий шматок серверного коду, яким застосунок, зроблений у Lovable, Bolt, Cursor чи Claude Code, користується для роботи, що не може відбуватися в браузері, наприклад для виклику OpenAI з ключем. Ви натискаєте її, і замість результату застосунок показує ось це:
Failed to send a request to the Edge Function
Функцію задеплоєно. Якщо викликати її з термінала, вона, можливо, навіть відповідає бездоганно. З вашого застосунку вона щоразу падає, і "failed to send a request to the Edge Function" звучить так, ніби ваш запит нікуди не пішов.
Цілком можливо, що пішов. Ваша функція могла відпрацювати, зробити свою роботу й відповісти, а браузер відмовився віддати цю відповідь вашому застосунку. Що саме з цього сталося, записано лише в одному місці, і це не та помилка, яку отримав ваш застосунок.
Що означає "Failed to send a request to the Edge Function"?
Це означає, що supabase-js попросив відповідь і не отримав жодної, яку йому дозволено прочитати.
Це речення записане незмінним текстом усередині
supabase-js
і належить помилці з назвою FunctionsFetchError. supabase-js кидає її, коли не
вдається сам запит, ще до того, як зʼявиться бодай якийсь код статусу. Цим вона й
відрізняється від своєї відомішої сестри:
Edge Function returned a non-2xx status code
означає, що відповідь повернулася і була помилкою. Ця ж означає, що жодної
відповіді, яку можна прочитати, не повернулося взагалі.
Запит падає так із кількох причин. Пристрій без мережі, адреса проєкту у вашому застосунку неправильна, або правила браузера для інших адрес відхилили відповідь. Ці правила називаються CORS, скорочення від Cross-Origin Resource Sharing, і вони не дають вебсторінці читати відповідь з іншої адреси, якщо сама відповідь цього не дозволяє. Коли функція відповідає з термінала й падає лише у вашому застосунку, підозрювати варто CORS, і браузер скаже, чи це він.
Ваш застосунок живе за однією адресою, скажімо yourapp.lovable.app, а ваша
функція за іншою, <project-ref>.supabase.co. Уявіть браузер як швейцара вашого
застосунку. Пошту з іншої адреси він віддає застосунку, лише якщо на ній є ярлик
відправника про те, що вашому застосунку її можна отримати: заголовок відповіді
з назвою Access-Control-Allow-Origin. Перед листом на кшталт вашого швейцар
спершу стукає у двері відправника й питає, чи той узагалі такий приймає. А коли
щось відхиляє, вашому застосунку каже лише, що не вийшло. Причину він записує у
свій журнал.
Третє повідомлення з тієї самої родини. Relay Error invoking the Edge Function
це supabase-js повідомляє, що relay Supabase не зміг дістатися вашої функції,
тож перевірте, чи немає проблеми в Supabase,
перш ніж змінювати код.
Чому ваш застосунок не дізнається більше
Бо браузер навмисно ховає причину від вашого коду.
Сторінка MDN про помилки CORS
каже це прямо: з міркувань безпеки код JavaScript не має доступу до подробиць
того, що пішло не так у запиті CORS. Ваш застосунок дізнається, що щось не
вдалося, і більше нічого. У Chrome помилка, яку обгортає supabase-js, звучить як
TypeError: Failed to fetch, і Chromium використовує цей текст однаково для
будь-якого мережевого збою, тож виводити її немає сенсу. За тією самою
сторінкою MDN, єдиний спосіб дізнатися, що сталося, це консоль браузера.
Це і є журнал швейцара. Відкрийте опублікований застосунок у Chrome або Edge, натисніть F12, оберіть Console і ще раз натисніть кнопку в застосунку. Потрібний рядок червоний і починається так:
Access to fetch at 'https://<project-ref>.supabase.co/functions/v1/<name>' from origin 'https://yourapp.lovable.app' has been blocked by CORS policy:
Усе після двокрапки і є причина. Firefox і Safari формулюють це інакше; цитати в цьому дописі взяті з Chrome, з вихідного коду Chromium.
Стук у двері має технічну назву, preflight request. Це запит із методом
OPTIONS, який браузер надсилає перед вашим, і supabase-js завжди його
спричиняє: він надсилає ваш ключ у заголовку Authorization, а дані у форматі
JSON, і посібник MDN про CORS
не відносить жодне з цього до того, що сторінка може надіслати, не спитавши
спершу.
Яка половина не вдалася, і чи запускалася ваша функція?
Це вирішують слова "preflight request" у рядку консолі. Якщо вони там є, браузер зупинився на стуку, і ваш запит так і не надіслали. Якщо їх немає, ваш запит пройшов, функція відпрацювала, а її відповідь затримали.
| Рядок консолі закінчується на | Що сталося | Чи зробила функція роботу? |
|---|---|---|
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. | На стук відповіли без ярлика | Ні. До неї дійшов лише стук |
Response to preflight request doesn't pass access control check: It does not have HTTP ok status. | На стук відповіли помилкою | Ні |
Request header field <name> is not allowed by Access-Control-Allow-Headers in preflight response. | Ваш застосунок надсилає заголовок, якого немає у списку функції | Ні |
No 'Access-Control-Allow-Origin' header is present on the requested resource., без згадки про preflight | Стук пройшов, ваш запит дійшов, а відповідь була без ярлика | Так |
| Жодного рядка про CORS | Не CORS: зʼєднання, адреса або тайм-аут, заданий вашим кодом | Перегляньте вкладку Invocations |
Перший рядок дає функція без обробника для OPTIONS.Сторінка Supabase про помилку 500
описує це з боку функції: стук доходить до вашого коду, як будь-який інший
запит, і функція пише в лог SyntaxError: Unexpected end of JSON input, бо стук
у двері не має вмісту, а код спробував його прочитати. Її відповідь є помилкою
без заголовків, і Chrome повідомляє про відсутній заголовок, ще не дивлячись на
статус. Якщо цей рядок є на вкладці Logs функції, ви знайшли причину.
Другий рядок означає, що стук отримав заголовки, а разом із ними помилку.
Один зі шляхів до цього: перевірка над гілкою для OPTIONS, яку стук не може
пройти, наприклад перевірка того, хто викликає. Стук не несе жодного з ваших
заголовків, зокрема й ключа, тож щоразу її не проходить.
На четвертому рядку варто зупинитися, бо роботу зроблено. Якщо функція зберігає рядок, надсилає лист чи списує гроші з картки, це вже сталося, і повторне натискання кнопки зробить це вдруге. Не вдалася відповідь, яка пішла без ярлика. Спершу шукайте помилку: збій, відповідь на який ваш код так і не встиг написати, або гілку помилки, що повертає відповідь без заголовків. Та сама сторінка Supabase попереджає, що тоді браузер звинувачує CORS у збої, справжня причина якого інша.
Тож прочитайте власний журнал функції, перш ніж чіпати будь-які заголовки. У панелі Supabase відкрийте Edge Functions, оберіть функцію й перегляньте її вкладки Invocations і Logs за ту хвилину, коли ви натиснули кнопку. Як прочитати справжню помилку розбирає обидві, а ще те, що означає кожен код статусу.
Останній рядок означає, що швейцар нічого не відхиляв. Запит не пройшов
дорогою: пристрій утратив зʼєднання, або адреси проєкту, яку викликає ваш
застосунок, не існує. Один випадок походить із вашого власного коду. Якщо він
передає timeout у supabase.functions.invoke,
supabase-js
скасовує запит, щойно час вичерпано, і повідомляє те саме речення, хоча функція,
можливо, ще працює.
Як виправити: відповісти на стук і причепити ярлик до кожної відповіді
Дві зміни у функції, і вони йдуть в одному деплої.
- Гілка для
OPTIONSнайпершою, до будь-якого рядка, що читає вміст або перевіряє того, хто викликає. Вона відповідає на стук заголовками CORS і нічим більше. - Заголовки CORS у кожній відповіді, яку повертає функція, і в успішних, і в помилках. Відповідь без них затримують, хоч би що в ній було.
Посібник Supabase про CORS будує функцію приблизно так:
import { corsHeaders } from 'npm:@supabase/supabase-js@^2/cors'
Deno.serve(async (req) => {
if (req.method === 'OPTIONS') {
return Response.json({ ok: true }, { headers: corsHeaders })
}
try {
const body = await req.json()
// the function's own work goes here
return Response.json({ done: true }, { headers: corsHeaders })
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return Response.json({ error: message }, { status: 400, headers: corsHeaders })
}
})
Заголовки беруться із самого supabase-js, а не зі списку, вставленого у функцію.
Посібник імпортує їх так, починаючи з версії supabase-js 2.95.0, щоб список
збігався з тим, що надсилають клієнтські бібліотеки, і радить задеплоїти
функцію заново після оновлення. Старіші функції мають написану вручну копію того
самого списку у файлі з назвою _shared/cors.ts. Саме з цієї копії береться
третій рядок таблиці: заголовок, який надсилає ваш застосунок і якого немає у
списку.
Якщо ви просите Lovable, Bolt, Cursor чи Claude Code зробити виправлення, ці два пронумеровані пункти і є промпт. Назвіть обидва. Другий легко пропустити, і функція, де є лише перший, під час наступного збою переходить із першого рядка таблиці на четвертий.
Новіші приклади Supabase загортають усю функцію у withSupabase, який
відповідає на стук за вас.
Як перенести секрет в Edge Function
показує цю обгортку разом із перевіркою того, хто викликає, і саме про цю
перевірку наступний розділ.
Коли заголовки є в кожній відповіді, функція, що падає, перестає показувати це
повідомлення й починає показувати Edge Function returned a non-2xx status code. Тепер відповідь доходить до вашого застосунку, і причина всередині неї.
Чи вирішує заголовок CORS, хто може викликати вашу функцію?
Ні. Він вирішує, які вебсторінки можуть прочитати відповідь, і дивляться на нього лише браузери.
Власний список Supabase встановлює Access-Control-Allow-Origin у *, що
дозволяє будь-яке джерело, і в копії, яку експортує supabase-js, і в старій,
написаній вручну. Для функції, що перевіряє токен того, хто викликає, це
нормально. Для функції, що нічого не перевіряє, це дає будь-якій сторінці
будь-якого сайту змогу викликати вашу функцію з браузерів своїх відвідувачів і
читати, що повертається.
Обмеження заголовка вашим власним доменом теж не закриває функцію. Скрипт, термінал чи інший сервер надсилає той самий запит без браузера, ніколи не читає заголовок і отримує всю відповідь. Що відкриває і чого не відкриває wildcard у CORS розбирає це докладно.
Замок на Edge Function: перевірка того, хто викликає, вписана у саму функцію. Найбільше вона важить для функцій, що опиняються за цією помилкою, бо саме вони тримають ключ OpenAI або секретний ключ вашого проєкту, а секретний ключ обходить кожне правило row level security, яке ви написали.
Інший вихід із помилки CORS полягає в тому, щоб перестати користуватися
функцією: покласти ключ у змінну з VITE_ і викликати OpenAI прямо зі сторінки.
Це працює одразу й кладе ключ у файл, який завантажує кожен відвідувач. Це
перше з двох виправлень, після яких вам гірше.
Цю помилку видно ззовні. Наше безкоштовне сканування читає JavaScript вашого
опублікованого застосунку так, як прочитав би сторонній, і називає кожен ключ,
який знаходить, зокрема й те, чи йому там місце. Це займає близько 20 секунд і
не потребує облікового запису:
проскануйте свій застосунок.
Щоб ключ не опинився на сторінці після наступної зміни
Функцію, яку ви виправили сьогодні, можуть переписати наступного разу, коли ви попросите свій білдер про нову функцію, а найшвидший спосіб обійти помилку CORS кладе ваш ключ на сторінку.
Reeve Monitor щогодини заново сканує ваш опублікований застосунок:
- усі девʼять перевірок щогодини, для трьох застосунків
- лист того дня, коли нове сканування оцінить ваш застосунок гірше за попереднє; секретний ключ у вашому JavaScript сам обмежує оцінку рівнем D
- чи застосунок доступний, кожні 60 секунд
- щомісячний звіт про те, що він бачив
Функція, що тримає секретний ключ вашого проєкту, може змінити будь-який рядок вашої бази для будь-кого, хто її викликає. Reeve Care тримає копію вашої бази Supabase там, куди ніщо у вашому проєкті не дістане.
- зашифрована копія за розкладом, у власному сховищі Reeve в ЄС, поза вашим обліковим записом Supabase
- кожна копія перевірена, перш ніж її зарахують, підрахунком рядків у кожній таблиці
- відновлення в один клік, яке зберігає те, що є зараз, перш ніж замінити
- ваші завантажені файли теж, щойно ви приєднаєте доступ до Storage
- усе, що робить Monitor
Що зробити зараз
Що робити
- Відкрийте консоль браузера в опублікованому застосунку, ще раз натисніть кнопку й прочитайте кінець червоного рядка про CORS.
- Якщо в рядку згадано preflight request, додайте на самий початок функції гілку для
OPTIONS, яка повертає заголовки CORS. - Якщо ні, ваша функція відпрацювала. Перегляньте її вкладки Invocations і Logs і перевірте, що вона вже зробила, перш ніж хтось знову натисне кнопку.
- Повертайте заголовки CORS у кожній відповіді, з помилками включно, й імпортуйте їх із supabase-js 2.95.0 або новішого, щоб список лишався актуальним.
- Тримайте у функції перевірку того, хто викликає. Заголовок CORS лише каже браузерам, хто може прочитати відповідь.
Коли помилка у вашому застосунку зміниться на Edge Function returned a non-2xx status code, заголовки працюють і відповідь доходить;
той допис читає її далі.
Десятихвилинний чекліст безпеки охоплює, що ще новий застосунок
зазвичай лишає відкритим.
Поширені запитання
Що означає "Failed to send a request to the Edge Function"?
Це означає, що supabase-js попросив відповідь у вашої Supabase Edge Function і не отримав жодної, яку йому дозволено прочитати. Або запит не пройшов дорогою, наприклад тому, що пристрій був без мережі, або браузер відмовився віддати відповідь вашому застосунку за своїми правилами для запитів на інші адреси, які називають CORS. Браузер навмисно ховає причину від вашого коду й пише її у свою консоль.
Чому моя Edge Function працює з термінала, але не з мого застосунку?
Бо CORS застосовують лише браузери. Термінал, скрипт чи інший сервер надсилає запит і читає всю відповідь. Браузер спершу перевіряє, чи відповідь дозволяє вашому застосунку її прочитати, а перед запитом на кшталт того, що надсилає supabase-js, ще й питає в функції дозволу окремим запитом OPTIONS. Якщо не вдається хоч один із цих кроків, ваш застосунок отримує цю помилку, хоча той самий виклик працює будь-де ще.
Чи запускалася моя Edge Function, якщо я бачу цю помилку?
Прочитайте червоний рядок у консолі браузера. Якщо в ньому згадано preflight request, браузер зупинився на запиті дозволу, і ваш запит так і не пішов. Якщо ні, запит пройшов, функція зробила свою роботу, а затримали лише відповідь. Тоді перегляньте вкладки Invocations і Logs функції, перш ніж знову натискати кнопку, бо друге натискання повторить роботу.
Як виправити помилку CORS у Supabase Edge Function?
Нехай функція першою справою відповідає на запит OPTIONS, ще до будь-якого коду, що читає вміст або перевіряє того, хто викликає, і повертає заголовки CORS у кожній відповіді, зокрема й у відповідях з помилкою. Посібник Supabase про CORS імпортує ці заголовки з supabase-js версії 2.95.0 або новішої, щоб список збігався з тим, що надсилає клієнт. Після зміни задеплойте функцію заново.
Що таке "Relay Error invoking the Edge Function"?
Це третє повідомлення supabase-js, яке зʼявляється, коли relay Supabase не зміг дістатися вашої функції. Це не відмова CORS і не відповідь вашого власного коду, тож спершу перевірте, чи немає проблеми в самого Supabase, а вже потім щось змінюйте.
Чи безпечно дозволяти будь-яке джерело в заголовках CORS моєї Edge Function?
Лише якщо функція перевіряє, хто її викликає. Заголовок CORS каже браузерам, які вебсторінки можуть прочитати відповідь; він нікому не заважає викликати функцію. Власні заголовки Supabase дозволяють будь-яке джерело. Для функції, що перевіряє токен того, хто викликає, це нормально, а функцію без перевірки так може використовувати будь-який сайт. Обмеження заголовка вашим доменом теж не закриває функцію від скриптів.