# CORS error calling an Edge Function

> Fix "blocked by CORS policy" when calling a SupaHost Edge Function from the browser: answer OPTIONS preflights, add headers to every response, and spot router errors hiding behind CORS.

Source: https://supahost.app/kb/function-cors-error · Updated 2026-11-11

## What you see

In the browser console:

```text
Access to fetch at 'https://app-one.acme.supahost.space/functions/v1/send-invoice'
from origin 'http://localhost:5173' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
```

supabase-js reports it as a `FunctionsFetchError` ("Failed to send a request to the Edge Function"). The same call works with curl or from a server.

## Why it happens

Browsers only let a page read a response from another origin if the response says it's allowed. Before a `POST` with JSON or an `Authorization` header, the browser sends an `OPTIONS` preflight request. Your function must answer it, and every real response must carry the CORS headers too. Edge Functions don't add these for you.

Three common slips:

- The function doesn't handle `OPTIONS` at all.
- The success path sets headers, but error responses (`400`, `500`) don't, so failures show up as CORS errors.
- `Access-Control-Allow-Headers` doesn't list a header the client sends (supabase-js sends `authorization`, `x-client-info`, `apikey` and `content-type`).

## How to fix it

1. **Create a shared headers file** at `_shared/cors.ts` in the function's files:

   ```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',
   }
   ```

   For a private tool, replace `*` with your app's origin, such as `https://invoices.example.com`.

2. **Answer preflights and add the headers everywhere:**

   ```ts
   import { corsHeaders } from './_shared/cors.ts'

   Deno.serve(async (req) => {
     if (req.method === 'OPTIONS') {
       return new Response('ok', { headers: corsHeaders })
     }
     try {
       const { invoiceId } = await req.json()
       return Response.json({ sent: invoiceId }, { headers: corsHeaders })
     } catch (err) {
       console.error(err)
       return Response.json({ error: 'Bad request' }, { status: 400, headers: corsHeaders })
     }
   })
   ```

   Recent versions of supabase-js (2.95.0 and later) also export a ready-made `corsHeaders` object from `@supabase/supabase-js/cors`.

3. **Add any custom headers** your app sends (for example `x-tenant-id`) to `Access-Control-Allow-Headers`.
4. **Deploy and retry.** Check DevTools → Network: the `OPTIONS` request should return `200` with the headers, followed by your real request.

## On SupaHost

`OPTIONS` requests skip the **Require JWT** check, so preflights reach your code without a token. But the router's own error responses, such as `401` for a missing JWT or `404` for an unknown slug, are produced before your code runs, so they don't carry the headers your code sets. When a browser gets such a response without an `Access-Control-Allow-Origin` header, it reports a CORS error that is really an auth or URL problem. In DevTools, look at the real request's status: if it's `401` or `404` with an `sb-error-code` header, fix that first. See [Edge Function returns 401](/kb/function-401) and [Edge Function returns 404](/kb/function-404).

## Related

- [CORS for browser calls](/docs/functions/cors)
- [Calling functions from your app](/docs/functions/calling-functions)
- [Edge Function returns 401](/kb/function-401)
- [Edge Function returns 404](/kb/function-404)
