Skip to content

Fetch implementation with cross-platform support #1457

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"dependencies": {
"@supabase/auth-js": "2.70.0",
"@supabase/functions-js": "2.4.4",
"@supabase/node-fetch": "2.6.15",
"node-fetch-native": "^1.6.6",
"@supabase/postgrest-js": "1.19.4",
"@supabase/realtime-js": "next",
"@supabase/storage-js": "2.7.1"
Expand Down
118 changes: 103 additions & 15 deletions src/SupabaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,66 @@ export default class SupabaseClient<
protected authUrl: URL
protected storageUrl: URL
protected functionsUrl: URL
protected rest: PostgrestClient<Database, SchemaName, Schema>
rest!: PostgrestClient<Database, SchemaName, Schema>
protected _rest?: PostgrestClient<Database, SchemaName, Schema>
protected storageKey: string
protected fetch?: Fetch
protected _fetch?: Fetch

/**
* Get the fetch implementation using lib/fetch.ts
* Uses the modernized cross-platform fetch implementation with full security features.
*
* SYNC COMPATIBILITY: Creates a sync-compatible wrapper around async fetchWithAuth
*/
get fetch(): Fetch {
if (!this._fetch) {
// Create a sync-compatible fetch that handles async initialization internally
this._fetch = this._createSyncFetchWrapper()
}
return this._fetch
}

/**
* Creates a synchronous fetch wrapper that handles async initialization internally
* This maintains compatibility while using the modernized fetch implementation
*/
private _createSyncFetchWrapper(): Fetch {
let fetchPromise: Promise<Fetch> | null = null
let resolvedFetch: Fetch | null = null

return async (input, init = {}) => {
// If we already have the resolved fetch, use it directly
if (resolvedFetch) {
return resolvedFetch(input, init)
}

// If we haven't started initializing, start now
if (!fetchPromise) {
fetchPromise = fetchWithAuth(
this.supabaseKey,
this._getAccessToken.bind(this),
this._customFetch
).then((authenticatedFetch) => {
resolvedFetch = authenticatedFetch
return authenticatedFetch
})
}

// Wait for initialization and then make the request
const authenticatedFetch = await fetchPromise
return authenticatedFetch(input, init)
}
}

protected _customFetch?: Fetch

protected changedAccessToken?: string
protected accessToken?: () => Promise<string | null>

protected headers: Record<string, string>

/**
* Create a new client for use in the browser.
* Create a new client for cross-platform use (Node.js, Deno, Bun, browsers, workers).
* @param supabaseUrl The unique Supabase URL which is supplied when you create a new project in your project dashboard.
* @param supabaseKey The unique Supabase Key which is supplied when you create a new project in your project dashboard.
* @param options.db.schema You can switch in between schemas. The schema needs to be on the list of exposed schemas inside Supabase.
Expand Down Expand Up @@ -97,6 +147,7 @@ export default class SupabaseClient<

this.storageKey = settings.auth.storageKey ?? ''
this.headers = settings.global.headers ?? {}
this._customFetch = settings.global.fetch

if (!settings.accessToken) {
this.auth = this._initSupabaseAuthClient(
Expand All @@ -118,16 +169,40 @@ export default class SupabaseClient<
})
}

this.fetch = fetchWithAuth(supabaseKey, this._getAccessToken.bind(this), settings.global.fetch)
// Fetch will be initialized lazily using fetchWithAuth from lib/fetch.ts

this.realtime = this._initRealtimeClient({
headers: this.headers,
accessToken: this._getAccessToken.bind(this),
accessToken: async () => {
try {
const token = await this._getAccessToken()
return token || ''
} catch (error) {
// Log but don't throw to avoid breaking realtime connection
console.warn(
'Failed to get access token for realtime:',
error instanceof Error ? error.message : String(error)
)
return ''
}
},
...settings.realtime,
})
this.rest = new PostgrestClient(new URL('rest/v1', baseUrl).href, {
headers: this.headers,
schema: settings.db.schema,
fetch: this.fetch,

// Defer PostgrestClient initialization to avoid circular dependency
// This will be initialized lazily when first accessed
Object.defineProperty(this, 'rest', {
get: () => {
if (!this._rest) {
this._rest = new PostgrestClient(new URL('rest/v1', baseUrl).href, {
headers: this.headers,
schema: settings.db.schema,
fetch: this.fetch,
})
}
return this._rest
},
configurable: true,
})

if (!settings.accessToken) {
Expand Down Expand Up @@ -269,13 +344,26 @@ export default class SupabaseClient<
}

private async _getAccessToken() {
if (this.accessToken) {
return await this.accessToken()
}
try {
if (this.accessToken) {
return await this.accessToken()
}

const { data } = await this.auth.getSession()
const { data, error } = await this.auth.getSession()

return data.session?.access_token ?? null
if (error) {
console.warn('Failed to get session:', error.message)
return null
}

return data.session?.access_token ?? null
} catch (error) {
console.warn(
'Error getting access token:',
error instanceof Error ? error.message : String(error)
)
return null
}
}

private _initSupabaseAuthClient(
Expand Down Expand Up @@ -308,7 +396,7 @@ export default class SupabaseClient<
lock,
debug,
fetch,
// auth checks if there is a custom authorizaiton header using this flag
// auth checks if there is a custom authorization header using this flag
// so it knows whether to return an error when getUser is called with no session
hasCustomAuthorizationHeader: 'Authorization' in this.headers,
})
Expand Down
Loading