apps/docs/content/troubleshooting/edge-function-401-error-response.mdx
A 401 response from an Edge Function means either:
Check the response body returned by the request
"Invalid JWT" or "Missing authorization header"{ "code": 401, "message": "Invalid JWT" }
{ "code": 401, "message": "Missing authorization header" }
Both of these messages come from the legacy auth verification check
Go to: Built-in JWT check failures
If the response body contains a message you coded, or nothing at all, then your function code did execute and returned a 401 itself.
Go to: Your function returned a 401
Run this query in Log Explorer to classify recent 401s:
select
cast(timestamp as datetime) as timestamp,
req.pathname as function_name,
case
when metadata.execution_id is not null then 'your_code_returned_401'
when metadata.execution_id is null
and (
new_auth.prefix is not null
or legacy_payload.algorithm != 'HS256'
) then 'incompatible_keys'
when metadata.execution_id is null
and (
(legacy_auth_data.invalid is not null or new_auth.error is not null)
or legacy_payload.algorithm = 'HS256'
) then 'invalid_key'
when metadata.execution_id is null
and legacy_auth_data is null
and new_auth.prefix is null then 'missing_auth_header'
end as cause
from
function_edge_logs
-- unnesting metadata
cross join UNNEST(metadata) as metadata
cross join UNNEST(metadata.request) as req
cross join UNNEST(metadata.response) as res
-- unnesting auth details
left join UNNEST(req.sb) as sb
left join UNNEST(sb.apikey) as apikey
left join UNNEST(apikey.authorization) as new_auth
left join UNNEST(sb.jwt) as legacy_jwt
left join UNNEST(legacy_jwt.authorization) as legacy_auth_data
left join UNNEST(legacy_auth_data.payload) as legacy_payload
where res.status_code = 401
order by timestamp desc
limit 50;
Depending on the output, you can use this table to find the appropriate debugging section:
| Value | Go to |
|---|---|
your_code_returned_401 | Your function returned a 401 |
incompatible_keys | Incompatible key format |
invalid_key | Invalid key |
missing_auth_header | Missing Authorization header |
Your function ran, and somewhere in your code, its logic returned a 401.
Example:
return new Response(JSON.stringify(data), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 401, // <-- you set this
})
How to fix:
401. Look for explicit status codes on Response objects.console.error('Returning 401 - reason:', reason)
See: Error handling in Edge Functions
Supabase Edge Functions have a legacy auth verification check that runs before your code. When it fails, your function never executes, and you get a 401 with "Invalid JWT" or "Missing authorization header" directly from the platform.
Supabase now recommends turning off this built-in check and managing authentication directly in your function code, giving you more control over access. See Securing Edge Functions.
</Admonition>The subsections below cover specific failure modes.
Your project uses the new asymmetric keys for authentication. However, the legacy auth verification check only understands the legacy format.
Fix: Disable the built-in JWT check using one of the below methods and optionally handle auth in your function code
<Accordion type="default" openBehaviour="multiple" chevronAlign="right" justified size="medium" className="text-foreground-light mt-8 mb-6"
<div className="border-b mt-3 pb-3"> <AccordionItem header="Method A: Dashboard" id="item-1" >
In the Functions Dashboard, open the affected function's detail tab and toggle off JWT verification.
Redeploy the edge function from the Supabase CLI with the --no-verify-jwt flag
supabase functions deploy YOUR_FUNCTION_NAME --no-verify-jwt
</AccordionItem>
curl 'https://api.supabase.com/v1/projects/PROJECT_ID/functions/FUNCTION_NAME' \
--request PATCH \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '{"verify_jwt": false}'
</AccordionItem>
The built-in check is enabled and the key you sent doesn't match your project's keys.
Fix (recommended): Disable the built-in check using the steps in Incompatible key format.
Fix (alternative): If you want to keep the built-in check, ensure you're sending a valid key. Use one of your legacy API keys with the Supabase client library when making your request.
const supabase = createClient('https://xyzcompany.supabase.co', 'anon-key-or-service_role-key')
The built-in check is enabled but your request has no Authorization header at all.
If you're using a Supabase client library, the header is added automatically. If you're calling the function from an external client (cURL, fetch, etc.), you need to supply it:
curl -L -X POST 'https://PROJECT_REF.supabase.co/functions/v1/hello-world' \
-H 'Authorization: Bearer YOUR_ANON_OR_SERVICE_ROLE_KEY' \
--data '{"name":"Functions"}'
Alternatively, you can disable the built-in check entirely (see Incompatible key format).