supabase-js: "XMLHttpRequest is not defined" on sveltekit endpoints when deployed on Vercel

Bug report

Describe the bug

Using the client for auth or for querying the database in a sveltekit endpoint throw an error :

{
  data: null,
  error: ReferenceError: XMLHttpRequest is not defined
      at file:///var/task/server/app.mjs:2522:21
      at new Promise (<anonymous>)
      at fetch2 (file:///var/task/server/app.mjs:2517:16)
      at file:///var/task/server/app.mjs:2646:7
      at new Promise (<anonymous>)
      at file:///var/task/server/app.mjs:2645:12
      at Generator.next (<anonymous>)
      at file:///var/task/server/app.mjs:2619:67
      at new Promise (<anonymous>)
      at __awaiter$8 (file:///var/task/server/app.mjs:2601:10)
}

To Reproduce

I made a repository exposing the issue : https://github.com/Pixselve/supabase-sveltekit-endpoints Click on a button to fetch the endpoint and observe the cloud functions logs on Vercel.

Expected behavior

Requests should not fail and should give the appropriate data.

System information

  • OS: Vercel Node.js Runtime
  • Version of supabase-js: 1.10.0
  • Version of Node.js: 14

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 27 (9 by maintainers)

Most upvoted comments

@sbutler-gh I think your repo isn’t actually using the up-to-date version of @supabase/supabase-js, see here:

sbutler-gh/just-start@c32ecef/package-lock.json#L325-L326

It appears it’s using version 1.24.0, and the ability to pass in a custom fetch implementation was added in 1.27.0 (and 1.28.1 is the latest version).

After running npm update to update packages and re-deploying, it now works as expected in production on Cloudflare Pages! (using fetch: (...args) => fetch(...args), didn’t try the other syntaxes yet.). Thank you SO MUCH @jacobwgillespie ! I can’t thank you enough!

Thanks to a very impressive series of PRs from @jacobwgillespie , this now has a workaround

https://github.com/supabase/supabase-js/releases/tag/v1.27.0

after upgrading to v1.27.0 you can use a custom fetch implementation

import { createClient } from '@supabase/supabase-js'

// Provide a custom `fetch` implementation as an option
const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key', { fetch: fetch })

Jacob - if you send your details (and tshirt size) to swag@supabase.io we will send you a nice package 🙏

It looks like FaunaDB solved this. Essentially, they added an extra config argument that allows users to opt-out of cross-fetch by passing in a custom fetch function.

Issue: https://github.com/fauna/faunadb-js/issues/207 PR: https://github.com/fauna/faunadb-js/pull/214

So a potential solution for this is:

import { createClient } from '@supabase/supabase-js'

export const supabase = createClient(
    import.meta.env.VITE_SUPABASE_URL,
    import.meta.env.VITE_SUPABASE_KEY,
    {
        fetch: import.meta.env.USE_NATIVE_FETCH ? fetch : undefined,
    }
)

If no fetch is provided, cross-fetch is used. @kiwicopple, would this be straightforward to implement?

I think I’ve gotten to the bottom of this.

  1. Serverless deployments require bundling of node_modules. That’s why you need noExternal to include all node dependencies otherwise you get errors during deployment. This causes dev mode to break, so only add node deps to noExternal when process.env.NODE_ENV = "production.

  2. Vite creates two bundles, a server bundle “functions/node/render/server/app.mjs” and a client bundle “static/_app/*”. The problem is that it only reads dependencies once:

https://github.com/vitejs/vite/blob/344d77e9735bc907b9383ad729afb0a8daa2af5f/packages/vite/src/node/plugins/resolve.ts#L466

First off, cross-fetch always resolves to the browser entry point. The way the entry point is resolved does not take into account whether the bundler is in SSR mode or not.

  1. But even if it did resolve correctly, Vite only does one pass over each dependency and then caches it. You can’t have the same package resolve to a different entry point for SSR unless you remove resolvedImports['.'] or flush it between each phase.

So my hacky workaround at the moment is to force disable the browser entry point during SSR module generation, and to disable module caching.

image

This will need to be fixed by Vite.

I’ve opened several PRs that allow specifying a custom fetch implementation as an option - ideally I think cross-fetch should support environments like Cloudflare Workers given its use in the ecosystem, but a custom fetch option enables a workaround today, gives flexibility for future environments (e.g. ones that don’t yet exist or are more esoteric), and allows you to provide a mocked fetch for testing or otherwise customize fetch based on your specific needs.

import { createClient } from '@supabase/supabase-js'

const supabase = createClient('url', 'key', { fetch: fetch })

The main @supabase/supabase-js library wraps other client libraries, so each needs a PR to enable the option.

Wrapped Clients

supabase-js (depends on the other three PRs)

For Next users wondering if this (v1.27+) allows Supabase client use in the middleware API - yes:

import { createClient } from '@supabase/supabase-js';
import { NextRequest, NextResponse } from 'next/server';

export async function middleware(request: NextRequest) {
  const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { fetch });
  …

This works great and there is not even need to rebind fetch using fetch.bind(self) or (...args) => fetch(...args)!

@PH4NTOMiki that’s probably unclear documentation on my part, I believe if you directly pass the native fetch function as the custom fetch, JavaScript will treat this as an “illegal invocation” as it tries to execute a native method in the context of the Supabase options argument rather than in the global context it was attached to.

You can pass a custom function or bind the fetch function to work around that issue. For Cloudflare Workers specifically, there is no global or window object like you’d have in other runtimes, but you do have self, so:

const supabase = createClient('...', '...', {fetch: fetch.bind(self)})

Really the example I added to the README should have probably been { fetch: customFetch } to avoid confusion, I can look at opening a few more PRs. 🙂

async function supabaseInsert (table, arg) {
 return fetch(`${supabaseUrl}/rest/v1/${table}`, {
  headers: {
    Apikey: supabaseKey,
    Authorization: `Bearer ${supabaseKey}`,
    "Content-Type": "application/json",
    Prefer: "return=representation"
  },
  method: "POST",
 body: JSON.stringify(arg)
})
}

async function supabaseFrom (table, filter) {
return fetch(`${supabaseUrl}/rest/v1/${table}?${filter}`, {
  headers: {
    Apikey: supabaseKey,
    Authorization: `Bearer ${supabaseKey}`,
  }
})
}

got it done after an hour or two of npm misadventures.

In any case, appreciate the prompt response @kiwicopple ! Supabase is working marvelously and took exactly one evening to actually get working. The exploration also introduced me to postgrest, which seems a phenomenal building block for supabase’s product! Cheers!