Vai al contenuto

Basi della sicurezza

"Failed to send a request to the Edge Function": la causa vera

"Failed to send a request to the Edge Function": il browser non ha ricevuto una risposta leggibile. La sua console dice perché e se la tua funzione è partita.

Vlad Tkachenko12 min di lettura
Una sbarra abbassata di traverso sul passaggio, con una busta trattenuta dall'altra parte e un contorno giallo vuoto dove dovrebbe pendere la sua etichetta.

In breve

  • "Failed to send a request to the Edge Function" vuol dire che supabase-js non ha ricevuto nessuna risposta che potesse leggere. O la richiesta è fallita per strada, o il browser ha trattenuto la risposta, e la tua app non viene a sapere altro, di proposito.
  • La console del browser ha il motivo. Una riga che cita la preflight request vuol dire che la tua richiesta non è mai partita. Una riga che non la cita vuol dire che la tua funzione è stata eseguita e solo la sua risposta è stata trattenuta.
  • La correzione è un ramo per OPTIONS in cima alla funzione e gli header CORS su ogni risposta, errori compresi. Quegli header dicono ai browser solo chi può leggere la risposta; chi può chiamare lo decide un controllo nel tuo codice.

La tua app ha un pulsante che chiama una Supabase Edge Function, quel piccolo pezzo di codice server che un'app costruita con Lovable, Bolt, Cursor o Claude Code usa per il lavoro che non può avvenire nel browser, come chiamare OpenAI con una chiave. Lo premi e, invece di un risultato, la tua app mostra questo:

Failed to send a request to the Edge Function

La funzione è pubblicata. Chiamata da un terminale, magari risponde anche benissimo. Dalla tua app fallisce ogni volta, e "failed to send a request to the Edge Function" fa pensare che la tua richiesta non sia mai partita.

Potrebbe benissimo essere partita. La tua funzione potrebbe essere stata eseguita, aver fatto il suo lavoro e risposto, e il browser si è rifiutato di consegnare la risposta alla tua app. Quale delle due cose sia successa è scritto in un solo posto, e non è l'errore che la tua app ha ricevuto.

Cosa vuol dire "Failed to send a request to the Edge Function"?

Vuol dire che supabase-js ha chiesto una risposta e non ne ha ricevuta nessuna che potesse leggere.

La frase è un testo fisso dentro supabase-js, legato a un errore chiamato FunctionsFetchError. supabase-js lo lancia quando fallisce la richiesta stessa, prima che esista un qualsiasi codice di stato da guardare. È questo che lo separa dal fratello più noto: Edge Function returned a non-2xx status code vuol dire che è tornata una risposta ed era un errore. Questo vuol dire che non è tornata nessuna risposta leggibile.

Una richiesta fallisce così per una manciata di motivi. Il dispositivo è offline, l'indirizzo del progetto nella tua app è sbagliato, oppure le regole del browser per gli altri indirizzi hanno rifiutato la risposta. Quelle regole si chiamano CORS, da Cross-Origin Resource Sharing, e impediscono a una pagina web di leggere una risposta che arriva da un altro indirizzo, a meno che la risposta non lo permetta. Quando la funzione risponde da terminale e fallisce solo nella tua app, il sospettato è CORS, e il browser ti dirà se è stato lui.

La tua app abita a un indirizzo, diciamo yourapp.lovable.app, e la tua funzione a un altro, <project-ref>.supabase.co. Pensa al browser come al portiere della tua app. La posta che arriva da un altro indirizzo viene consegnata alla tua app solo se porta un'etichetta del mittente che dice che la tua app può averla: un header di risposta chiamato Access-Control-Allow-Origin. Per una lettera come la tua, il portiere prima bussa alla porta del mittente per chiedere se la accetta. E quando rifiuta qualcosa, alla tua app dice solo che non ha funzionato. Il motivo lo scrive nel suo registro.

Un terzo messaggio è della stessa famiglia. Relay Error invoking the Edge Function è supabase-js che segnala che il relay di Supabase non è riuscito a raggiungere la tua funzione, quindi controlla se Supabase ha un problema prima di toccare il codice.

Perché la tua app non viene a sapere altro

Perché il browser nasconde di proposito il motivo al tuo codice.

La pagina di MDN sugli errori CORS lo dice chiaramente: per motivi di sicurezza, il codice JavaScript non ha accesso ai dettagli di cosa è andato storto in una richiesta CORS. La tua app sa che qualcosa è fallito e nient'altro. In Chrome l'errore che supabase-js avvolge dice TypeError: Failed to fetch, un testo che Chromium usa uguale per qualsiasi guasto di rete, quindi stamparlo non aggiunge niente. Secondo la stessa pagina di MDN, l'unico modo per sapere cosa è successo è la console del browser.

Quello è il registro del portiere. Apri la tua app pubblicata in Chrome o Edge, premi F12, scegli Console e premi di nuovo il pulsante nella tua app. La riga che cerchi è rossa e comincia così:

Access to fetch at 'https://<project-ref>.supabase.co/functions/v1/<name>' from origin 'https://yourapp.lovable.app' has been blocked by CORS policy:

Tutto quello che segue i due punti è il motivo. Firefox e Safari lo scrivono in un altro modo; le citazioni di questo articolo sono quelle di Chrome, prese dal codice sorgente di Chromium.

Il bussare alla porta ha un nome tecnico, preflight request. È una richiesta con il metodo OPTIONS che il browser invia prima della tua, e supabase-js ne fa sempre partire una: manda la tua chiave in un header Authorization e i tuoi dati come JSON, e la guida CORS di MDN non mette nessuna delle due cose fra quelle che una pagina può inviare senza chiedere prima.

Sopra, il bussare viene rifiutato e la tua richiesta non parte mai. Sotto, il bussare passa, la funzione fa il lavoro e solo la sua risposta viene trattenuta.

Quale metà è fallita, e la tua funzione è stata eseguita?

Le parole "preflight request" nella riga della console lo decidono. Se ci sono, il browser si è fermato al bussare e la tua richiesta non è mai stata inviata. Se non ci sono, la tua richiesta è passata, la tua funzione è stata eseguita e la sua risposta è stata trattenuta.

La riga della console finisce conCosa è successoLa funzione ha fatto il lavoro?
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.Al bussare hanno risposto senza etichettaNo. Le è arrivato solo il bussare
Response to preflight request doesn't pass access control check: It does not have HTTP ok status.Al bussare hanno risposto con un erroreNo
Request header field <name> is not allowed by Access-Control-Allow-Headers in preflight response.La tua app invia un header che la lista della funzione non nominaNo
No 'Access-Control-Allow-Origin' header is present on the requested resource., senza citare preflightIl bussare è passato, la tua richiesta è arrivata, la risposta era senza etichettaSì
Nessuna riga CORSNon è CORS: la connessione, l'indirizzo o un timeout impostato dal tuo codiceGuarda la scheda Invocations

La prima riga è quello che produce una funzione senza gestore per OPTIONS. La pagina di Supabase sull'errore 500 lo descrive dal lato della funzione: il bussare raggiunge il tuo codice come qualsiasi altra richiesta, e la funzione scrive nel log SyntaxError: Unexpected end of JSON input, perché un bussare alla porta non porta contenuto e il codice ha provato a leggerne uno. La sua risposta è un errore senza header, e Chrome segnala l'header mancante prima di guardare lo stato. Se quella riga è nella scheda Logs della funzione, l'hai trovato.

La seconda riga vuol dire che il bussare ha ricevuto gli header, e un errore insieme. Un modo per arrivarci è un controllo messo sopra il ramo per OPTIONS che il bussare non può superare, come un controllo su chi chiama. Il bussare non porta nessuno dei tuoi header, nemmeno la tua chiave, quindi fallisce ogni volta.

La quarta riga è quella su cui rallentare, perché il lavoro è stato fatto. Se la funzione salva una riga, manda un'email o addebita una carta, è fatto, e premere di nuovo il pulsante lo rifà. A fallire è stata la risposta, uscita senza etichetta. Cerca prima un errore: un crash, la cui risposta il tuo codice non ha fatto in tempo a scrivere, o un ramo di errore che restituisce senza gli header. La stessa pagina di Supabase avverte che allora il browser dà la colpa a CORS per un fallimento la cui causa vera è un'altra.

Quindi leggi il registro della funzione prima di toccare qualsiasi header. Nella dashboard di Supabase apri Edge Functions, scegli la funzione e leggi le sue schede Invocations e Logs al minuto in cui hai premuto il pulsante. Leggere l'errore vero le passa in rassegna entrambe, insieme a cosa vuol dire ogni codice di stato.

L'ultima riga vuol dire che il portiere non ha rifiutato niente. La richiesta è fallita per strada: il dispositivo ha perso la connessione, oppure l'indirizzo di progetto che la tua app chiama non esiste. Un caso viene dal tuo codice. Se passa un timeout a supabase.functions.invoke, supabase-js annulla la richiesta quando il tempo scade e segnala questa stessa frase, anche se la funzione magari sta ancora lavorando.

Come correggere: rispondere al bussare ed etichettare ogni risposta

Due modifiche nella funzione, e vanno nello stesso deploy.

  1. Un ramo per OPTIONS come primissima cosa, prima di qualsiasi riga che legga il contenuto o controlli chi chiama. Risponde al bussare con gli header CORS e nient'altro.
  2. Gli header CORS su ogni risposta che la funzione restituisce, quelle riuscite e gli errori. Una risposta senza di loro viene trattenuta, qualunque cosa dica.

La guida CORS di Supabase imposta una funzione più o meno così:

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 })
  }
})

Gli header arrivano da supabase-js stesso invece che da una lista incollata nella funzione. La guida li importa così a partire dalla versione 2.95.0 di supabase-js, perché la lista corrisponda a quello che le librerie client inviano, e dice di rifare il deploy dopo un aggiornamento. Le funzioni più vecchie portano una copia scritta a mano della stessa lista, in un file chiamato _shared/cors.ts. Da quella copia viene la terza riga della tabella: un header che la tua app invia e che la lista non nomina.

Se chiedi la correzione a Lovable, Bolt, Cursor o Claude Code, quei due punti numerati sono il prompt. Nominali entrambi. Il secondo si dimentica facilmente, e una funzione che ha solo il primo passa dalla prima riga della tabella alla quarta al prossimo crash.

Gli esempi più recenti di Supabase avvolgono l'intera funzione in withSupabase, che risponde al bussare al posto tuo. Spostare un segreto in una Edge Function mostra questo involucro con un controllo su chi chiama, e quel controllo è l'argomento della sezione successiva.

Con gli header su ogni risposta, una funzione che va in crash smette di mostrare questo messaggio e comincia a mostrare Edge Function returned a non-2xx status code. La risposta adesso raggiunge la tua app, con il motivo dentro.

Il browser cerca l'etichetta e non guarda mai cosa dice la risposta. Un errore che porta gli header raggiunge la tua app; uno senza viene trattenuto e segnalato come richiesta fallita.

L'header CORS decide chi può chiamare la tua funzione?

No. Decide quali pagine web possono leggere la risposta, e solo i browser lo guardano.

La lista di Supabase stesso imposta Access-Control-Allow-Origin a *, che consente qualsiasi origine, sia nella copia che supabase-js esporta sia in quella vecchia scritta a mano. Su una funzione che controlla il token di chi chiama, va bene. Su una funzione che non controlla niente, permette a qualsiasi pagina di qualsiasi sito di chiamare la tua funzione dai browser dei propri visitatori e di leggere quello che torna.

Restringere l'header al tuo dominio non chiude nemmeno la funzione. Uno script, un terminale o un altro server invia la stessa richiesta senza browser, non legge mai l'header e riceve la risposta intera. Cosa apre e cosa no un jolly CORS lo spiega nel dettaglio.

La serratura di una Edge Function è un controllo su chi chiama, scritto dentro la funzione. Conta soprattutto sulle funzioni che finiscono dietro questo errore, perché sono quelle che tengono una chiave OpenAI o la chiave segreta del tuo progetto, e la chiave segreta scavalca tutte le regole di row level security che hai scritto.

L'altra via d'uscita da un errore CORS è smettere di usare la funzione: mettere la chiave in una variabile VITE_ e chiamare OpenAI direttamente dalla pagina. Funziona subito, e mette la chiave nel file che ogni visitatore scarica. È la prima delle due correzioni che ti lasciano messo peggio. Questo errore si vede da fuori. La nostra scansione gratuita legge il JavaScript della tua app pubblicata come farebbe uno sconosciuto e nomina ogni chiave che trova, dicendo anche se è una di quelle che devono stare lì. Ci vogliono circa 20 secondi e non serve un account: scansiona la tua app.

Tenere la chiave fuori dalla pagina dopo la prossima modifica

Una funzione che hai corretto oggi può essere riscritta la prossima volta che chiedi una funzionalità al tuo builder, e il modo più rapido per aggirare un errore CORS è quello che mette la tua chiave nella pagina.

Reeve Monitor scansiona di nuovo la tua app pubblicata ogni ora:

  • tutti e nove i controlli ogni ora, su un massimo di tre app
  • una email il giorno in cui una nuova scansione valuta la tua app peggio della precedente; una chiave segreta nel tuo JavaScript limita da sola il voto a D
  • se l'app risponde, ogni 60 secondi
  • un rapporto mensile di quello che ha visto

Una funzione che tiene la chiave segreta del tuo progetto può cambiare qualsiasi riga del tuo database per chiunque la chiami. Reeve Care tiene una copia del tuo database Supabase dove niente nel tuo progetto può arrivare.

  • una copia cifrata secondo una pianificazione, conservata sullo storage di Reeve nell'UE, fuori dal tuo account Supabase
  • ogni copia verificata prima di contare, contando le righe di ogni tabella
  • un ripristino in un clic che salva quello che c'è adesso prima di sostituirlo
  • anche i tuoi file caricati, appena colleghi una credenziale di Storage
  • tutto quello che fa Monitor

Cosa fare adesso

Cosa fare

  • Apri la console del browser sulla tua app pubblicata, premi di nuovo il pulsante e leggi la fine della riga CORS rossa.
  • Se la riga cita la preflight request, aggiungi in cima alla funzione un ramo per OPTIONS che restituisca gli header CORS.
  • Se non la cita, la tua funzione è stata eseguita. Leggi le sue schede Invocations e Logs e controlla cosa ha già fatto prima che qualcuno prema di nuovo il pulsante.
  • Restituisci gli header CORS su ogni risposta, errori compresi, e importali da supabase-js 2.95.0 o successivo perché la lista resti aggiornata.
  • Tieni dentro la funzione un controllo su chi chiama. L'header CORS dice ai browser solo chi può leggere la risposta.

Quando l'errore nella tua app diventa Edge Function returned a non-2xx status code, gli header funzionano e la risposta passa; quell'articolo la legge da lì in avanti. La checklist di sicurezza in 10 minuti copre cos'altro un'app appena lanciata tende a lasciare aperto.

FAQ

Cosa vuol dire "Failed to send a request to the Edge Function"?

Vuol dire che supabase-js ha chiesto una risposta alla tua Supabase Edge Function e non ne ha ricevuta nessuna che potesse leggere. O la richiesta è fallita per strada, per esempio perché il dispositivo era offline, o il browser si è rifiutato di consegnare la risposta alla tua app secondo le sue regole sulle richieste verso altri indirizzi, chiamate CORS. Il browser nasconde di proposito il motivo al tuo codice e lo scrive nella sua console.

Perché la mia Edge Function funziona da terminale ma non dalla mia app?

Perché solo i browser applicano CORS. Un terminale, uno script o un altro server invia la richiesta e legge tutta la risposta. Un browser controlla prima che la risposta dica che la tua app può leggerla, e prima di una richiesta come quella che invia supabase-js chiede anche il permesso alla funzione con una richiesta OPTIONS separata. Se uno dei due passi fallisce, la tua app riceve questo errore mentre la stessa chiamata funziona ovunque altrove.

La mia Edge Function è stata eseguita se ricevo questo errore?

Leggi la riga rossa nella console del browser. Se cita la preflight request, il browser si è fermato alla richiesta di permesso e la tua richiesta non è mai partita. Se non la cita, la richiesta è passata, la funzione ha fatto il suo lavoro e solo la risposta è stata trattenuta. In quel caso leggi le schede Invocations e Logs della funzione prima di premere di nuovo il pulsante, perché un secondo clic ripete il lavoro.

Come correggo un errore CORS su una Supabase Edge Function?

Fai rispondere la funzione per prima cosa alla richiesta OPTIONS, prima di qualsiasi codice che legga il contenuto o controlli chi chiama, e restituisci gli header CORS su ogni risposta, comprese quelle di errore. La guida CORS di Supabase importa quegli header da supabase-js, versione 2.95.0 o successiva, così la lista corrisponde a quello che il client invia. Fai di nuovo il deploy della funzione dopo la modifica.

Cos'è "Relay Error invoking the Edge Function"?

È un terzo messaggio di supabase-js, che arriva quando il relay di Supabase non è riuscito a raggiungere la tua funzione. Non è un rifiuto CORS né una risposta del tuo codice, quindi controlla prima se è Supabase ad avere un problema, prima di cambiare qualsiasi cosa.

È sicuro consentire qualsiasi origine negli header CORS della mia Edge Function?

Solo se la funzione controlla chi la chiama. L'header CORS dice ai browser quali pagine web possono leggere la risposta; non impedisce a nessuno di chiamare la funzione. Gli header di Supabase stesso consentono qualsiasi origine, il che va bene per una funzione che controlla il token di chi chiama e lascia che qualunque sito usi una funzione che non lo fa. Restringere l'header al tuo dominio non chiude nemmeno la funzione agli script.

Scritto da

Vlad Tkachenko

Fondatore di Reeve

Passo le giornate a guardare app costruite con Lovable, Bolt, v0, Cursor e Replit, e la breve lista di errori che vi ricompaiono di continuo.

Altro sull'autore

Da leggere dopo

Tutti gli articoli

Fai il backup del database Supabase prima della prossima modifica

Reeve ne tiene una copia cifrata a intervalli programmati e la ripristina per te se qualcosa va storto.

Vedi i backup Supabase

Controllo esterno automatizzato, non un audit completo. L'assenza di risultati non è una garanzia di sicurezza.