Fix guides / Supabase: "Edge Function returned a non-2xx status code"

How to fix "Edge Function returned a non-2xx status code"

Your app calls a Supabase Edge Function (to send an email, call OpenAI, create a Stripe checkout) and all you get is FunctionsHttpError: Edge Function returned a non-2xx status code. This message is generic on purpose. The real reason is one step away.

Symptoms

  • The browser console or a toast shows Edge Function returned a non-2xx status code.
  • It worked in the builder preview or locally, then broke after deploy or after rotating keys.
  • Asking the AI builder to "fix the edge function" changes code but the error stays.

Step 1: read the real error in your app

supabase.functions.invoke throws a FunctionsHttpError when the function ran but returned a 4xx or 5xx. The body your function sent is inside it:

import { FunctionsHttpError, FunctionsRelayError, FunctionsFetchError } from '@supabase/supabase-js';

const { data, error } = await supabase.functions.invoke('create-checkout', { body: { plan: 'pro' } });
if (error instanceof FunctionsHttpError) {
  const details = await error.context.json().catch(() => error.context.text());
  console.error('Function said:', error.context.status, details);
} else if (error instanceof FunctionsRelayError) {
  console.error('Relay error:', error.message);
} else if (error instanceof FunctionsFetchError) {
  console.error('Could not reach the function:', error.message);
}

Step 2: read the function logs

In the Supabase dashboard open Edge Functions, choose the function, then Logs (and Invocations for status codes). A crash shows as an uncaught error with a line number. If there is nothing at all, the request never reached your code: see the 401 section below.

The usual causes and fixes

1. A secret is missing in production (most common)

Local .env files and builder settings do not travel to the deployed function. In the dashboard open Edge Functions, then Secrets (or run supabase secrets set OPENAI_API_KEY=...) and add every key the function reads with Deno.env.get. Fail loudly so you see it next time:

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

2. 401 Unauthorized before your code runs

By default functions require a valid JWT. If the user is logged out, or you call the function with fetch without an Authorization: Bearer header, Supabase rejects it with 401 and your code never runs. Use supabase.functions.invoke (it sends the session token), make sure the user is signed in, or for public endpoints like webhooks deploy with JWT verification off (supabase functions deploy stripe-webhook --no-verify-jwt, or verify_jwt = false for that function in supabase/config.toml).

3. The body is not what the function expects

await req.json() throws on an empty or non-JSON body. Pass a plain object as body to invoke (it is JSON-encoded for you) and validate inputs, returning a 400 with a clear message instead of crashing.

4. The upstream API failed

OpenAI, Resend, or Stripe returned an error (bad key, no credits, wrong price ID) and the function passed it on as a 500. Log and return the upstream message:

const r = await fetch(url, opts);
if (!r.ok) {
  const text = await r.text();
  console.error('Upstream error', r.status, text);
  return new Response(JSON.stringify({ error: 'Upstream failed', status: r.status, detail: text }), { status: 502, headers: { ...corsHeaders, 'Content-Type': 'application/json' } });
}

5. An unhandled crash

Wrap the handler in try/catch, log the error, and always return a response with your CORS headers. A crash without CORS headers shows in the browser as a CORS error, which sends people looking in the wrong place.

Before you redeploy

Redeploy the function after changing code (secrets apply without a redeploy). Test it directly with curl using your anon key or a user token so you can see the raw status and body without the front end in the way.

Still stuck? No shame in it: these bugs hide in settings your AI builder cannot see. Send us the error and your repo and we will fix it as a pull request within 24 hours, explained in plain English, for $49. Not fixed means full refund.
Request a fix

Last updated 2026-10-19.