Skip to content

Guides

Lovable app shows "Failed to fetch" or a CORS error after deploy

Niall Hackett ·

A Lovable app that says "Failed to fetch" (or "Failed to send a request to the Edge Function") after deploy usually has one of four problems. The Supabase edge function doesn't answer the browser's OPTIONS preflight. The function crashes and returns an error without CORS headers. The function only allows one origin. Or the live build has a missing or wrong VITE_SUPABASE_URL/anon key. Open DevTools → Network, find the failing request, and read the function logs before you change any code.

Why does a CORS error show up as "Failed to fetch"?

Your app runs on one origin (yourapp.lovable.app or your own domain), and your edge function runs on another (<project>.supabase.co/functions/v1/...). Because supabase-js sends custom headers (authorization, apikey, x-client-info, content-type), the browser first sends an OPTIONS preflight. If that preflight, or the real response after it, is missing Access-Control-Allow-* headers, the browser drops the response, and your code only gets a generic network error (Supabase CORS guide, Supabase KB: Unable to call Edge Function).

How do I tell which cause I have?

  1. Open the live app with DevTools → Network and trigger the feature.
  2. Find the two requests to the function, the OPTIONS request and the POST/GET.
    • OPTIONS fails (red): the function ignores preflight, the function name is wrong or not deployed, or the project URL is wrong.
    • OPTIONS passes, the real request fails: the function probably crashed. Open Supabase → Edge Functions → your function → Logs. In Lovable Cloud, look under the Cloud → Edge functions view.
    • ERR_NAME_NOT_RESOLVED: the Supabase URL in the build is missing or stale.
  3. Read the end of the console message. If it says "Response to preflight request doesn't pass access control check", it's a preflight problem. If it just says "No 'Access-Control-Allow-Origin' header is present", look for a crash first.

People report this exact pattern. In one r/lovable thread, a user traced "Failed to fetch" to an edge function that hard-coded Access-Control-Allow-Origin: https://lovable.dev while the app ran on a different domain. Supabase GitHub issues #16146 and #34731 show the same "blocked by CORS policy… preflight" error, from localhost and from a production domain.

How do I add CORS headers and OPTIONS handling to a Supabase edge function?

If you use @supabase/supabase-js v2.95.0 or later, import the headers from the SDK so the allow-list stays in sync with what the client sends (docs):

TypeScript
// supabase/functions/my-function/index.ts
import { corsHeaders } from 'npm:@supabase/supabase-js@^2/cors'

Deno.serve(async (req) => {
  // 1. Answer the preflight before anything else (don't call req.json() here)
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  try {
    const { name } = await req.json()
    return new Response(JSON.stringify({ message: `Hello ${name}!` }), {
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
    })
  } catch (error) {
    // 2. Errors need CORS headers too, or the browser hides the real message
    const message = error instanceof Error ? error.message : String(error)
    return new Response(JSON.stringify({ error: message }), {
      status: 400,
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
    })
  }
})

On older SDK versions, use a shared file, as the Supabase KB shows:

TypeScript
// supabase/functions/_shared/cors.ts
export const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
}

Then redeploy the function. Editing the code doesn't change what's already running.

What if I want to allow only my own domains?

A wildcard is fine for most public functions that check the JWT. If you lock the function to specific origins, list every origin the app actually runs on: preview, *.lovable.app, and your custom domain with and without www.

TypeScript
const ALLOWED = new Set([
  'https://yourapp.lovable.app',
  'https://yourdomain.com',
  'https://www.yourdomain.com',
])

function cors(req: Request) {
  const origin = req.headers.get('Origin') ?? ''
  return {
    'Access-Control-Allow-Origin': ALLOWED.has(origin) ? origin : 'https://yourdomain.com',
    'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
    'Vary': 'Origin',
  }
}

Could missing secrets or env vars be the real cause?

Yes, and often they are. A function that reads a secret that exists only in your local .env (for example RESEND_API_KEY) throws, returns a 500 without CORS headers, and the browser shows a CORS error. Set function secrets in Supabase (Edge Functions → Secrets, or supabase secrets set) and fail loudly:

TypeScript
const key = Deno.env.get('RESEND_API_KEY')
if (!key) {
  return new Response(JSON.stringify({ error: 'RESEND_API_KEY is not set' }), {
    status: 500,
    headers: { ...corsHeaders, 'Content-Type': 'application/json' },
  })
}

On the frontend, check the Supabase client config at startup, so a bad build fails clearly instead of with "Failed to fetch":

TypeScript
// src/integrations/supabase/client.ts
import { createClient } from '@supabase/supabase-js'

const url = import.meta.env.VITE_SUPABASE_URL
const anonKey = import.meta.env.VITE_SUPABASE_ANON_KEY ?? import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY

if (!url || !anonKey) {
  throw new Error('Missing VITE_SUPABASE_URL or anon/publishable key in this build')
}

export const supabase = createClient(url, anonKey)

If you host the build outside Lovable (Vercel, Netlify), add these variables there and rebuild, because Vite bakes them in at build time. See blank page in production from missing env vars.

Do Supabase redirect URLs cause CORS errors?

Not directly. Auth redirect URLs control where Supabase sends users after login, magic links, or OAuth. If your live domain isn't in Authentication → URL Configuration (Site URL + Redirect URLs), login sends people back to the preview or localhost, or fails, and that can look like a fetch problem (Supabase redirect URLs docs). Add every production domain there. More in Supabase login works locally but not live.

The request succeeds but no data comes back. Is that CORS?

No. If the Network tab shows a 200 with [], or a 401/403/42501, CORS is fine, and Row Level Security is blocking the query. See Supabase RLS policy error.

FAQ

Is "Failed to send a request to the Edge Function" the same as a CORS error?

Usually it means the same thing: the browser couldn't complete the request. The cause is most often missing preflight handling, a crash without CORS headers, a wrong function name, or a function that isn't deployed (Supabase KB).

Should I set mode: 'no-cors' on fetch?

No. You get an opaque response your code can't read. Fix the headers on the function instead.

Should I add CORS headers in supabase.functions.invoke() on the client?

No. Access-Control-Allow-* are response headers, so only the server (your edge function) can send them.

It works in the Lovable preview but not on my custom domain. Why?

Usually the function allows only certain origins, the custom domain is missing from the Auth redirect URLs, or the external host's build lacks the Supabase env vars.

Do I need to redeploy after fixing CORS?

Yes. Redeploy the edge function, and rebuild the frontend if you changed env vars.


Not sure what else broke when you went live? ReplayWatch runs a free scan of your deployed AI-built app. It checks that the page loads, looks at security headers, flags secrets exposed in your HTML/JS, tests for public Supabase/Firebase reads, finds broken links and assets, reviews SEO tags, and confirms your signup page is reachable. It doesn't log in or run signup flows, so use the DevTools steps above for edge function calls behind auth. ReplayWatch can then monitor the app so you hear about the next break before your users do.