supabase-js icon indicating copy to clipboard operation
supabase-js copied to clipboard

Unexpected CORS errors encountered in HTTP DELETE Edge Function invocations

Open AverageCakeSlice opened this issue 8 months ago • 6 comments

Bug report

  • [x] I confirm this is a bug with Supabase, not with my own application.
  • [x] I confirm I have searched the Docs, GitHub Discussions, and Discord.

Describe the bug

The edge function runtime throws CORS errors in the browser when attempting to invoke a function for HTTP DELETE methods. I haven't tested beyond HTTP DELETE and POST, but I suspect that this issue may apply to other methods as well.

To Reproduce

  1. Create an HTTP DELETE edge function, follow the guides to develop locally and configure CORS as instructed in the docs here
  2. In a web application, make a call to supabase.functions.invoke and attempt to invoke your function using the 'DELETE' method.
  3. Observe the network tab in your browser's dev tools window. Note that an unexpected CORS error is thrown, even if your CORS headers are configured and applied to responses correctly according to the documentation.

Here is the code that causes the request to fail.

    const { error } = await supabase.functions.invoke('delete-self', {
        method: 'DELETE',
    })

Expected behavior

I should be able to call this endpoint without encountering CORS errors like I can with POST requests. Additionally, the docs for supabase.functions.invoke should be updated to allow the body option to be undefined for DELETE requests.

Screenshots

Image

Workaround

You can still successfully invoke these functions, but you must use the HTTP POST method to do so. Here's an example of a working invocation:

    const { error } = await supabase.functions.invoke('delete-self', {
        body: {},
        method: 'POST',
    })

You'll also need to ensure that your edge function is adjusted to allow POST requests instead of DELETE requests.

System information

  • OS: macOS Sequoia 15.5
  • Browser: Brave Browser 1.79.123
  • Version of supabase-js: 2.49.1
  • Version of Node.js: 22.14.0

Additional context

Here's the full content of my edge function and the contents of _shared/cors.ts

// _shared/cors.ts
export const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
// delete-self/index.ts
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
import { createClient } from 'jsr:@supabase/supabase-js@2'
import { corsHeaders } from '../_shared/cors.ts' 

Deno.serve(async (req) => {
    if (req.method === 'OPTIONS') {
        return new Response('ok', {
            headers: corsHeaders
        })
    }

    if (req.method !== 'DELETE') {
        return new Response('Method not allowed', {
            headers: {
                ...corsHeaders,
                'Allow': 'DELETE'
            },
            status: 405
        })
    }


    try {
        // Create a Supabase client with the Auth context of the logged-in user
        const supabaseClient = createClient(Deno.env.get('SUPABASE_URL') as string, Deno.env.get('SUPABASE_ANON_KEY') as string, {
            global: {
                headers: {
                    Authorization: req.headers.get('Authorization') as string,
                },
            },
        })

        const token = req.headers.get('Authorization')?.replace('Bearer ', '')
        const { data: { user }, error: userError } = await supabaseClient.auth.getUser(token)
        if (userError) throw userError
        if (!user) throw new Error('Failed to get user')
        // Sign out the user globally
        await supabaseClient.auth.signOut()

        // Create a separate service role client to handle the actual deletion action, since the user is logged out, and 
        // the delete action is a service role action
        const serviceRoleClient = createClient(Deno.env.get('SUPABASE_URL') as string, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') as string)

        const { error: deleteUserError } = await serviceRoleClient.auth.admin.deleteUser(user.id)
        if (deleteUserError) throw deleteUserError

        return new Response(JSON.stringify({
            message: 'User deleted successfully.',
        }), {
            headers: {
                ...corsHeaders,
                'Content-Type': 'application/json',
            },
            status: 200,
        })
    } catch (error: any) {
        return new Response(JSON.stringify({
            error: error?.message,
        }), {
            headers: {
                ...corsHeaders,
                'Content-Type': 'application/json',
            },
            status: 400,
        })
    }
})

AverageCakeSlice avatar Jun 20 '25 03:06 AverageCakeSlice

Have you checked the function log on the dashboard?

If there is a problem with the function, CORS header may be removed when the gateway returns the response instead. Instead of calling the function via the API in supabase-js, sending the request via curl is also a good way to find out what the problem is.

nyannyacha avatar Jun 20 '25 06:06 nyannyacha

I can invoke the functions via cURL/Postman without a problem. It's when I try invoking it from the supabase JS client in the browser that I run into problems.

AverageCakeSlice avatar Jun 23 '25 02:06 AverageCakeSlice

I can invoke the functions via cURL/Postman without a problem. It's when I try invoking it from the supabase JS client in the browser that I run into problems.

If so, we should probably move this issue to supabase-js.

nyannyacha avatar Jun 23 '25 02:06 nyannyacha

I don't believe Curl/Postman do options requests at least by default.

GaryAustin1 avatar Jun 24 '25 22:06 GaryAustin1

@AverageCakeSlice I was able to reproduce your issue and confirm its a CORS-related problem specific to DELETE method (and other non simple methods)

its from the lack of proper CORS headers in the preflight (OPTIONS) response

how to resolve the CORS errors:

Deno.serve(async (req) => {
  const origin = req.headers.get("origin") || "*";

  const corsHeaders = {
    'Access-Control-Allow-Origin': origin, // * is not valid when credentials like authorization are used
    'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
    'Access-Control-Allow-Methods': 'DELETE', // needed to allow DELETE requests
  };

  if (req.method === 'OPTIONS') {
    return new Response('ok', {
      headers: corsHeaders
    });
  }

  if (req.method !== 'DELETE') {
    return new Response('invalid method');
  }

  return new Response('deleted', {
    headers: {
      ...corsHeaders,
      'Content-Type': 'text/plain'
    }
  });
});

its not a bug but i think this should be mentioned in the docs for clarity

this issue can be closed since this is not a bug in supabase-js

GAURAV-DEEP01 avatar Jun 26 '25 22:06 GAURAV-DEEP01

This issue has been automatically marked as stale because it has not had any activity for 6 months. It will be closed in 30 days if no further activity occurs.

If this is still relevant, please comment to keep it open.

github-actions[bot] avatar Jan 11 '26 00:01 github-actions[bot]

I have created this project here as a playground: https://github.com/mandarini/test-edge

I can invoke all CRUD functions without an issue.

  • Here is my cors.ts file: https://github.com/mandarini/test-edge/blob/main/supabase/functions/_shared/cors.ts

As @GAURAV-DEEP01 very correctly pointed out, you needed to allow the method in the cors config. You are right, we should enhance the docs there. I am closing this issue, and pinging the docs team.

mandarini avatar Jan 28 '26 14:01 mandarini